import type { Extension, HealthcareService } from 'fhir/r4'; import { fromFhirMoney, toFhirMoney } from '../utils/money'; // Timed service pricing model. FHIR HealthcareService has no native per-time-unit rate field, so these two // Akashic extensions carry it - byte-identical URLs across plugin + infra. This is the foundation for billing // appointments by elapsed time (e.g. hourly infusion chairs, 15-minute therapy increments) rather than (or // alongside) a flat `service-price`. Rate is stored as FHIR Money (decimal); read/written in integer cents, // same convention as `servicePrice.ts`. export const SERVICE_TIME_RATE_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/StructureDefinition/service-time-rate'; export const SERVICE_TIME_UNIT_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/StructureDefinition/service-time-unit'; // The billing increment a timed rate is quoted against. `session` is a sentinel meaning "one whole // appointment" rather than a sub-division of time - it never divides a duration, it always yields 1. export type TimeUnit = 'hour' | '30min' | '15min' | 'session'; // Minutes per unit. `session` returns 0 as a sentinel (it has no minute length of its own). export const unitMinutes = (unit: TimeUnit): number => { switch (unit) { case 'hour': return 60; case '30min': return 30; case '15min': return 15; case 'session': return 0; default: return 0; } }; // The service's per-unit rate and unit, if both are set (and the rate is non-negative). Undefined = not a // timed service. export const getServiceTimeRate = ( service?: HealthcareService | null, ): { rateCents: number; unit: TimeUnit } | undefined => { const extensions = service?.extension ?? []; const money = extensions.find((e) => e.url === SERVICE_TIME_RATE_EXTENSION_URL)?.valueMoney; const unit = extensions.find((e) => e.url === SERVICE_TIME_UNIT_EXTENSION_URL)?.valueCode as | TimeUnit | undefined; if (!money || typeof money.value !== 'number' || !unit) { return undefined; } const rateCents = fromFhirMoney(money); return rateCents >= 0 ? { rateCents, unit } : undefined; }; // Strip-then-append BOTH the rate and unit extensions (mirrors `applyServicePrice`'s pattern). Passing // `rateCents: undefined` removes both. Preserves all other extensions untouched, so this composes safely // alongside `service-price` and `service-default-duration`. Drops the `extension` array entirely if it ends // up empty. export const applyServiceTimeRate = ( service: HealthcareService, rateCents: number | undefined, unit: TimeUnit, ): HealthcareService => { const others: Extension[] = (service.extension ?? []).filter( (e) => e.url !== SERVICE_TIME_RATE_EXTENSION_URL && e.url !== SERVICE_TIME_UNIT_EXTENSION_URL, ); const next: Extension[] = rateCents === undefined ? others : [ ...others, { url: SERVICE_TIME_RATE_EXTENSION_URL, valueMoney: toFhirMoney(rateCents) }, { url: SERVICE_TIME_UNIT_EXTENSION_URL, valueCode: unit }, ]; // - keep the empty array; see applyServicePrice for the full reasoning. An explicit `[]` // means "curated, deliberately none" and stops the server's carry-forward from resurrecting a // cleared rate; collapsing to `undefined` would be indistinguishable from a non-preserving client. return { ...service, extension: next }; }; // Whether the service has a valid timed rate configured. export const isTimedService = (service?: HealthcareService | null): boolean => getServiceTimeRate(service) !== undefined; // How many billing units an appointment of `durationMinutes` consumes at the given unit. `session` always // resolves to 1 (the whole appointment is the unit). Otherwise, duration / unitMinutes, rounded to 2 decimal // places. Guards non-positive or non-finite results to 0. export const computeTimedQuantity = (durationMinutes: number, unit: TimeUnit): number => { if (unit === 'session') { return 1; } const minutes = unitMinutes(unit); const raw = minutes > 0 ? Math.round((durationMinutes / minutes) * 100) / 100 : 0; return Number.isFinite(raw) && raw > 0 ? raw : 0; };