import type { Extension, HealthcareService } from 'fhir/r4'; /** * HOW MANY FUTURE APPOINTMENTS A PATIENT MAY HOLD WITH ONE PRACTITIONER FOR THIS SERVICE. * * Ruled 2026-09-06: *"No cap on a patient's future appointments by default; an optional practice setting * per service may limit them; the guard never applies to a request."* * * ABSENT MEANS NO CAP, and that default is the whole of the ruling rather than a convenience. The * booking guard behaved as though every service were capped at one, so a patient holding one appointment * with each practitioner who offers a service could not book that service at all and was told to * telephone. Nothing was configured to make that true; it was the only behaviour there was. * * WHY A NUMBER RATHER THAN A SWITCH, when the value in use may only ever be one. A switch stores the * ANSWER; a number stores the QUESTION the practice was asked. If the limit later needs to be two, a * switch has to be migrated and every reader re-taught, whereas this already says what it means. It * costs nothing to store the general case here, and the editor is free to offer only the values the * product supports. * * IT IS PER SERVICE, NOT PER PRACTICE. A practice may cap an annual physical without capping a * follow-up, which is why the ruling put it on the service. */ export const SERVICE_FUTURE_APPOINTMENT_LIMIT_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/StructureDefinition/service-future-appointment-limit'; /** * The configured limit, or `undefined` when this service is uncapped. * * REJECTS ZERO AND NEGATIVES AS UNCONFIGURED rather than honouring them. A stored `0` would mean "this * service can never be booked", which is not a cap - it is a closure, and a practice that wants that * removes the service or takes it off the practitioner. Reading it as a cap would let one bad write * silently make a service unbookable with nothing on screen to explain it, which is the failure this * whole card exists to end. * * A non-integer is refused for the same reason: 1.5 appointments is not a thing the practice meant, and * rounding somebody's data is worse than ignoring it. */ export const getServiceFutureAppointmentLimit = ( service?: HealthcareService | null, ): number | undefined => { const value = (service?.extension ?? []).find( (extension) => extension.url === SERVICE_FUTURE_APPOINTMENT_LIMIT_EXTENSION_URL, )?.valueInteger; if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) { return undefined; } return value; }; /** Is this service capped at all? The question most callers actually have. */ export const serviceCapsFutureAppointments = (service?: HealthcareService | null): boolean => getServiceFutureAppointmentLimit(service) !== undefined; /** * Strip-then-append, mirroring `applyServicePrice`. Passing `undefined` removes the cap. * * KEEPS AN EMPTY ARRAY rather than collapsing to `extension: undefined`, and that is load-bearing for * the same reason it is on price: an omitted key is byte-identical to the bare transform output, which * the server reads as "this client never considered extensions" and answers by carrying the STORED ones * forward. So collapsing here would make removing the last feature extension silently resurrect it on * the next save. An explicit `[]` is how a caller says the omission was deliberate; the server * normalizes it away before writing, since HealthLake rejects empty arrays. */ export const applyServiceFutureAppointmentLimit = ( service: HealthcareService, limit: number | undefined, ): HealthcareService => { const others: Extension[] = (service.extension ?? []).filter( (extension) => extension.url !== SERVICE_FUTURE_APPOINTMENT_LIMIT_EXTENSION_URL, ); // An unusable value REMOVES the cap rather than storing something no reader will honour. Writing a // 0 that `getServiceFutureAppointmentLimit` then ignores would leave the editor showing a cap the // booking path does not enforce - two screens disagreeing about the same setting. const usable = typeof limit === 'number' && Number.isInteger(limit) && limit >= 1; return { ...service, extension: usable ? [...others, { url: SERVICE_FUTURE_APPOINTMENT_LIMIT_EXTENSION_URL, valueInteger: limit as number, }] : others, }; };