import type { Appointment } from 'fhir/r4'; import { appointmentVersion, COMPANIONS_UNCONFIRMED } from '../appointmentVersion'; import { isRecordChanged } from '../recordChanged'; import type { SaveOperation, SaveTransport } from '../saveOperation'; import { settleUnknownWrite, writeOutcomeUnknown } from '../appointmentWriteOutcome'; /** A staff change that first deals with a clinician's visits may take longer than one save. */ export const STAFF_APPOINTMENT_BATCH_DEADLINE_MS = 120_000; export type StaffCancelWrite = ( appointment: Appointment, options: { basedOn: string; onFollowUp: (followUps: string[]) => void; transport?: SaveTransport }, ) => Promise; /** A cancellation whose answer never came and which a fresh read could not settle; nothing after it was sent. */ export class UnconfirmedCancellationError extends Error { constructor() { super('We could not confirm whether an appointment was cancelled, so nothing else was changed. Check the appointments before trying again.'); this.name = 'UnconfirmedCancellationError'; } } /** * Cancels each visit as it was when the dialog read it, one at a time, inside the dialog's save. A visit changed * since is refused by the server and stops the batch, so nothing after it is cancelled on an old reading. One whose * answer never came is read again before anything else is sent: cancelled counts; anything else is unconfirmed and * stops the batch. Returns what each saved cancellation left unfinished on the schedule. */ export async function cancelStaffAppointments( appointments: readonly Appointment[], operation: SaveOperation, updateAppointment: StaffCancelWrite, readNow: (appointmentId: string) => Promise, /** Filled as each cancellation is saved, so what one left unfinished survives a later failure. */ followUps: string[][] = [], ): Promise { for (const appointment of appointments) { const basedOn = appointmentVersion(appointment); let unfinished: string[] = []; try { await operation.step(() => updateAppointment({ ...appointment, status: 'cancelled' }, { basedOn, onFollowUp: (reported) => { unfinished = reported; }, transport: operation.transport, }), true); } catch (error) { if (!writeOutcomeUnknown(error)) throw error; const settled = await settleUnknownWrite(() => readNow(appointment.id as string), (stored) => stored.status === 'cancelled'); if (settled === 'unknown') throw new UnconfirmedCancellationError(); unfinished = [COMPANIONS_UNCONFIRMED]; } followUps.push(unfinished); } return followUps; } /** What to say when a visit changed while the dialog was open, or the dialog is too old to name one. */ export const appointmentsChangedMessage = (error: unknown, nothingDone: string): string | undefined => { if (isRecordChanged(error)) { return `An appointment changed while this was open, so ${nothingDone}. Close this and open it again to see the appointments as they are now.`; } if (error instanceof UnconfirmedCancellationError) return error.message; return undefined; };