import type { Questionnaire, QuestionnaireResponse, Task } from 'fhir/r4'; export const packetAnswerId = (task: Task): string | undefined => /^qpa-[0-9a-f]{40}-m\d{1,2}$/.test(task.id ?? '') ? `${task.id}-r` : undefined; const MEMBER_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/questionnaire-packet-member'; export const RESPONSE_EDITION_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/questionnaire-response-edition'; /** The ids the server's named-edition read accepts; anything else is never requested. */ export const PACKET_RESOURCE_ID = /^[A-Za-z0-9\-.]{1,64}$/; export const PIN_UNREADABLE = 'This form\'s assigned edition could not be read. Reload it to continue.'; export type PacketPin = { questionnaireId: string; version: string }; /** * A packet form's pinned edition, exactly as the server wrote it on the member; standalone work has none. * Packet work whose pin is missing, repeated or malformed is refused, never answered on the latest form. */ export function packetPinOf(task: Task): PacketPin | undefined { const members = (task.extension ?? []).filter(each => each.url === MEMBER_EXTENSION); if (!packetAnswerId(task) && members.length === 0) return undefined; const versions = members.length === 1 ? (members[0].extension ?? []).filter(each => each.url === 'version') : []; const version = versions.length === 1 ? versions[0].valueId : undefined; const questionnaireId = /^Questionnaire\/([^/]+)$/.exec(task.focus?.reference ?? '')?.[1]; if (!packetAnswerId(task) || !version || !PACKET_RESOURCE_ID.test(version) || !questionnaireId || !PACKET_RESOURCE_ID.test(questionnaireId)) { throw new Error(PIN_UNREADABLE); } return { questionnaireId, version }; } /** The edition read back is the form and edition asked for, before anything is shown or sent on it. */ export const isPinnedEdition = (questionnaire: Questionnaire | undefined, pin: PacketPin): boolean => questionnaire?.resourceType === 'Questionnaire' && questionnaire.id === pin.questionnaireId && questionnaire.meta?.versionId === pin.version && (questionnaire.item === undefined || Array.isArray(questionnaire.item)); const answeredOn = (response: QuestionnaireResponse, pin: PacketPin): boolean => { const declared = (response.extension ?? []).filter(each => each.url === RESPONSE_EDITION_EXTENSION); const form = /^(?:Questionnaire\/)?([^/]+)$/.exec(response.questionnaire ?? '')?.[1]; return form === pin.questionnaireId && declared.length === 1 && declared[0].valueId === pin.version; }; /** Packet members must prove the same saved edition on every read path. */ export function validatePacketAnswer(task: Task, response: QuestionnaireResponse, requestedId: unknown): void { const id = packetAnswerId(task); if (!id) return; const pin = packetPinOf(task)!; if (!task.for?.reference || !response || response.resourceType !== 'QuestionnaireResponse' || response.id !== id || requestedId !== id || !response.meta?.versionId || response.subject?.reference !== task.for?.reference || response.basedOn?.[0]?.reference !== `Task/${task.id}` || !answeredOn(response, pin)) { throw new Error('The saved answers could not be matched to this form. Reload it to continue.'); } if (!['in-progress', 'completed', 'amended'].includes(response.status)) { throw new Error('The saved answer is no longer available to continue.'); } } /** Read before retrying a first save: a lost answer may already have been persisted. */ export async function continuePacketAnswer(task: Task, io: { readResponse: (id: string) => Promise; readTask: (id: string) => Promise; writeTask: (task: Task, transport: { headers: Record; admit: () => void }) => Promise; current: () => boolean; }): Promise<{ response: QuestionnaireResponse; task: Task } | undefined> { const id = packetAnswerId(task); if (!id) return undefined; const check = () => { if (!io.current()) throw new Error('This form opening has ended. Reopen it to continue.'); }; check(); let response: QuestionnaireResponse; try { response = await io.readResponse(id); } catch (error) { if ((error as { status?: number }).status === 404) { check(); return undefined; } throw error; } check(); validatePacketAnswer(task, response, id); const fresh = await io.readTask(task.id!); check(); // The form as it is now must still pin the edition these answers were checked against. const pinned = packetPinOf(task)!; const now = packetPinOf(fresh); if (fresh.id !== task.id || fresh.for?.reference !== task.for?.reference || now?.questionnaireId !== pinned.questionnaireId || now?.version !== pinned.version || ['cancelled', 'entered-in-error', 'failed', 'rejected', 'draft'].includes(fresh.status)) { throw new Error('This form is no longer available to answer.'); } const completed = response.status === 'completed' || response.status === 'amended'; if (!completed && response.status !== 'in-progress') { throw new Error('The saved answer is no longer available to continue.'); } if (fresh.status === 'completed' && !completed) { throw new Error('The saved draft cannot reopen a completed form. Reload it to continue.'); } const reference = `QuestionnaireResponse/${id}`; if (fresh.output?.some(each => each.valueReference?.reference === reference)) return { response, task: fresh }; if (!fresh.meta?.versionId) throw new Error('The current form version could not be read. Reload it to continue.'); const linked = await io.writeTask({ ...fresh, status: completed ? 'completed' : 'in-progress', output: [...(fresh.output ?? []), { type: { text: 'QuestionnaireResponse' }, valueReference: { reference } }], }, { headers: { 'If-Match': `W/"${fresh.meta.versionId}"` }, admit: check }); check(); return { response, task: linked }; }