import type { CarePlan, CodeableConcept, Goal, Observation, Patient, Task } from 'fhir/r4'; import { ORGANIZATION_TAG_SYSTEM } from './carePlanAuthorship'; import { AKASHIC_TASK_MEASUREMENT_CODE_EXTENSION, AKASHIC_TASK_MEASUREMENT_UNIT_EXTENSION, } from './measurementTask'; import { AKASHIC_GOAL_DIRECTION_EXTENSION, AKASHIC_GOAL_START_VALUE_EXTENSION, type GoalDirection, } from './goalProgress'; import { readingQuantity } from './trackableMeasures'; // What each Add control on the plan editor writes. // // THESE SHAPES ARE READ BACK BY EVERYTHING ELSE. `describeActivity` decides what a row says from them, // `goalProgress` derives a bar from them, `getTaskCarePlanReference` finds the plan from them, and the // patient's task modal offers a reading field because of them. So a builder that omits a field does not // fail here - it fails silently three surfaces away, as a row that says the wrong thing or a bar that // never moves. Their tests assert through those readers rather than on the fields, for that reason. const TASK_TYPE_SYSTEM = 'https://akashichealthrecords.com/fhir/CodeSystem/task-type'; const DUE_MODE_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/task-due-mode'; const RECURRENCE_FREQUENCY_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/task-recurrence-frequency'; const RECURRENCE_ANCHOR_DATE_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/task-recurrence-anchor-date'; export type PlanRecurrence = 'daily' | 'weekly' | 'monthly' | 'annually'; export interface NewCarePlanInput { patient: Patient; title: string; organizationId: string; } /** * A plan starts as a DRAFT that belongs to this practice. * * The organization tag is not bookkeeping: the authorship rule fails closed without it, so an untagged * plan cannot be edited by anybody - including the person who just wrote it. * * `draft` because the drawing's footer says so - it is written across the visit and handed over with * "Give it to the patient", which is the moment it becomes theirs to see. */ export const buildDraftCarePlan = ({ patient, title, organizationId }: NewCarePlanInput): CarePlan => ({ resourceType: 'CarePlan', status: 'draft', intent: 'plan', title, subject: { reference: `Patient/${patient.id}` }, meta: { tag: [{ system: ORGANIZATION_TAG_SYSTEM, code: organizationId }] }, }); const planActivityBase = (plan: CarePlan, patient: Patient, description: string): Task => ({ resourceType: 'Task', status: 'requested', intent: 'order', description, for: { reference: `Patient/${patient.id}` }, // How every surface finds the plan a task came from - and the reason the row can offer "View care // plan" at all. basedOn: [{ reference: `CarePlan/${plan.id}`, display: plan.title }], code: { coding: [{ system: TASK_TYPE_SYSTEM, code: 'custom' }] }, }); export interface NewInstructionInput { plan: CarePlan; patient: Patient; description: string; /** Present makes it a one-off job; absent makes it standing. */ dueDate?: string; } /** * An instruction. * * WITH a due date it is a job with an end - "book your three-month review" - and stays tickable. * WITHOUT one it is STANDING, and `describeActivity` will mark it untickable: "take metformin twice * daily" is how the patient lives, not something to tick off. * * STANDING IS SAID OUT LOUD RATHER THAN LEFT AS AN ABSENCE, and that is not tidiness. Measured * 2026-09-07 on a real visit: a Task carrying no due mode and no due date was read by the server as * "due by the next appointment", bound to the visit it was written in and given that visit's end time. * `restriction.period.end` is the first thing `isStandingInstruction` looks at, so the instruction came * back describing itself as a one-off job and became tickable on every surface at once - the exact * thing the design says it must never be. Silence could not mean standing, so it stopped being silent. */ export const buildPlanInstruction = ({ plan, patient, description, dueDate, }: NewInstructionInput): Task => { const task = planActivityBase(plan, patient, description); if (!dueDate) { return { ...task, extension: [{ url: DUE_MODE_EXTENSION, valueCode: 'standing' }], }; } return { ...task, extension: [{ url: DUE_MODE_EXTENSION, valueCode: 'date' }], restriction: { period: { end: dueDate } }, }; }; export interface NewMeasurementInput { plan: CarePlan; patient: Patient; description: string; /** What to measure. It MUST match what the readings will be coded as, or no goal will ever move. */ measure: CodeableConcept; unit: string; /** Absent for a one-off reading. */ frequency?: PlanRecurrence; /** * The day the FIRST reading is due, as a date-only string. Defaults to today. * * REQUIRED BY THE SERVER FOR A RECURRING TASK, and it refuses the write without one - measured in * the product: `400 "Recurring Custom Tasks require a first due date"`. It is also what the * successor gate steps from, so a recurring activity without it has no series at all. */ firstDueDate?: string; } /** * Today, date-only, in UTC. * * UTC BECAUSE THE RECURRENCE ENGINE IS. The server's due dates are date-only and its parse and format * helpers are UTC throughout, so taking the local day here would put the first occurrence in a different * calendar from the arithmetic that steps it forward. */ const todayUtc = (): string => new Date().toISOString().slice(0, 10); /** * A measurement, which asks the patient for a number. * * THE RECURRENCE IS WHAT HANDS IT TO THE SUCCESSOR GATE. Without the recurring due-mode the server never * mints the next occurrence, so a weekly measurement would be answered once and never asked again - and * nothing would say so. */ export const buildPlanMeasurement = ({ plan, patient, description, measure, unit, frequency, firstDueDate, }: NewMeasurementInput): Task => { // Due TODAY rather than a whole interval out. A weekly weigh-in written at the visit is something // the patient can do now, and dating the first one a week ahead would leave the plan looking empty // for a week after the conversation that created it. const due = firstDueDate ?? todayUtc(); return { ...planActivityBase(plan, patient, description), extension: [ { url: AKASHIC_TASK_MEASUREMENT_CODE_EXTENSION, valueCodeableConcept: measure }, { url: AKASHIC_TASK_MEASUREMENT_UNIT_EXTENSION, valueString: unit }, ...(frequency ? [ { url: DUE_MODE_EXTENSION, valueCode: 'recurring' }, { url: RECURRENCE_FREQUENCY_EXTENSION, valueCode: frequency }, // The date every later occurrence is computed FROM. Absent, the server falls back to // the current due date, which drifts a monthly series every time it is answered late. { url: RECURRENCE_ANCHOR_DATE_EXTENSION, valueDate: due }, ] : []), ], ...(frequency ? { restriction: { period: { end: due } } } : {}), }; }; export interface NewGoalInput { plan: CarePlan; patient: Patient; description: string; /** The same measure the measurement writes. A mismatch is a goal that silently never moves. */ measure: CodeableConcept; startValue: number; targetValue: number; unit: string; direction: GoalDirection; } /** * A goal: where the patient started, where they are going, and which way is progress. * * THE MEASURE MUST MATCH THE MEASUREMENT'S. It is the commonest way a goal quietly never moves - the * target measures one thing and the readings are coded as another, so the bar sits at nought while the * patient logs faithfully. */ export const buildPlanGoal = ({ plan, patient, description, measure, startValue, targetValue, unit, direction, }: NewGoalInput): Goal => ({ resourceType: 'Goal', lifecycleStatus: 'active', description: { text: description }, subject: { reference: `Patient/${patient.id}` }, addresses: [{ reference: `CarePlan/${plan.id}`, display: plan.title }], target: [{ measure, detailQuantity: { value: targetValue, unit } }], extension: [ { url: AKASHIC_GOAL_DIRECTION_EXTENSION, valueCode: direction }, { url: AKASHIC_GOAL_START_VALUE_EXTENSION, valueQuantity: { value: startValue, unit } }, ], }); export interface NewOwnGoalInput { patient: Patient; description: string; measure: CodeableConcept; startValue: number; targetValue: number; unit: string; direction: GoalDirection; } /** * A goal the patient set for themselves, with no plan attached. * * NOBODY PRESCRIBED IT, AND THE RECORD HAS TO SAY SO. `expressedBy` naming the patient is what makes the * goals page draw "you set this one" rather than attributing it to a practice - and it is the case the * brief calls the proof that goals are not owned by practices. A practitioner cannot change it and does * not need to. * * NO `addresses`, rather than an empty array. A goal that belongs to no plan is a fact; an empty list of * plans is a shape that reads as one whose plan went missing. */ export const buildOwnGoal = ({ patient, description, measure, startValue, targetValue, unit, direction, }: NewOwnGoalInput): Goal => ({ resourceType: 'Goal', lifecycleStatus: 'active', description: { text: description }, subject: { reference: `Patient/${patient.id}` }, expressedBy: { reference: `Patient/${patient.id}` }, target: [{ measure, detailQuantity: { value: targetValue, unit } }], extension: [ { url: AKASHIC_GOAL_DIRECTION_EXTENSION, valueCode: direction }, { url: AKASHIC_GOAL_START_VALUE_EXTENSION, valueQuantity: { value: startValue, unit } }, ], }); export interface NewOwnReadingInput { goal: Goal; patient: Patient; value: number; /** When it was taken, as an ISO instant. */ now: string; } /** * A reading the patient logged against their own goal. * * IT CARRIES THE GOAL'S OWN MEASURE AND UNIT, taken from the goal rather than passed in. That is the * single commonest way a goal quietly never moves, and the only way to make it impossible is to give the * caller no opportunity to supply a different one. * * NO DERIVED ID, unlike a reading answering a plan activity. There is no occurrence to key on here - the * patient is logging whenever they choose - so each is a new reading, and two weigh-ins on one day are * two facts rather than a retry of the same one. * * `performer` IS THE PATIENT, which is what makes the goals page say "you logged it": a weight the * patient logged and one taken at the practice are not the same kind of fact. */ export const buildOwnReading = ({ goal, patient, value, now }: NewOwnReadingInput): Observation => { const measure = goal.target?.[0]?.measure; const unit = goal.target?.[0]?.detailQuantity?.unit; if (!measure) { throw new Error('A reading needs to know what the goal measures'); } return { resourceType: 'Observation', status: 'final', code: measure, subject: { reference: `Patient/${patient.id}` }, performer: [{ reference: `Patient/${patient.id}` }], effectiveDateTime: now, valueQuantity: readingQuantity(value, unit), }; };