// What finishing an encounter has to do about video, as one enumeration. // // THE DEFECT THIS EXISTS FOR (journey C11, 2026-09-13): `endCallIfActive` asked `phase === 'idle'` - // *am I in a call?* - when a Finish handler needs to know *is there a live meeting for this // encounter?* A practitioner who pressed Leave, wrote the note and then pressed Finish took the idle // path, so nothing was ended: the encounter flipped to `finished` while the session stayed live with // the patient in it. // // AND NOBODY COULD RECOVER IT. The server's join gate only runs on JOIN, so once the encounter is // finished every route back into that room answers 409 `ENCOUNTER_NOT_IN_PROGRESS`. The patient is in // a room no one can re-enter to end. That is the stranded-patient failure the end-for-everyone rule // exists to prevent, reached by a different door - and the leftover "Rejoin" the walk actually // reported is the small half of it. // // Pure, for the same reason `encounterVideoEntry` beside it is: the provider it lives in has no test // harness, and a decision this consequential should not be reachable only through one. import type { AvailableMeeting, CallPhase } from '../../services/video/callMachine'; import type { VideoContext } from '../../services/video/videoContract'; export type EncounterFinishVideoAction = /** No call and no live meeting for this encounter - the in-person case, and the common one. */ | { kind: 'nothing' } /** The finisher is in the call. End it for everyone, through the session they are holding. */ | { kind: 'end-my-call' } /** The finisher LEFT, and the visit outlived them. End the session they are no longer in. */ | { kind: 'end-left-session'; sessionId: string }; interface Args { /** This encounter's context - the scope. Absent means a caller that cannot say, so nothing. */ context?: VideoContext; phase: CallPhase; /** The encounter the caller's own call belongs to, when they are in one. */ callContext?: VideoContext; /** The meeting on offer. `endCall` re-offers one that was still live when you left it. */ available?: AvailableMeeting; } export function encounterFinishVideoAction({ context, phase, callContext, available }: Args): EncounterFinishVideoAction { // Anchored on `idle` rather than listing the live phases, so a phase added later fails SAFE - it // ends the call rather than leaving a meeting running. // A call for a DIFFERENT encounter is left alone: finishing A must never end B. if (phase !== 'idle' && (!context || !callContext || callContext === context)) return { kind: 'end-my-call' }; // SCOPED, NEVER THE BARE SLOT. There is one `available` app-wide, so an unscoped read would let // finishing encounter A destroy a live visit offered for patient B - a different patient, in a // different room, with no idea why it ended. if (!context || available?.context !== context) return { kind: 'nothing' }; return { kind: 'end-left-session', sessionId: available.sessionId }; }