import type { IValueSet } from './ValueSetService.tsx'; import { useCallback } from 'react'; import type { Bundle, ServiceRequest } from 'fhir/r4'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; import type { ReferralRequestScope } from '../utils/referralRequestScope'; import { readReferralContacts } from '../utils/referralContactHistory'; export type { ReferralRequestScope }; import type { ReferralClosureReason } from '../utils/referralClosureReason'; import { fetchAllPagesAsBundle } from '../utils/fetchAllPagesAsBundle'; import { searchPageSize } from '../utils/pageSizeOverride'; import { recipientContactExtension } from '../utils/patientReferrals'; import { toRow, type ReferralQueueResponse, type ReferralQueueRow } from '../hooks/useReferralQueue.ts'; // The three ways a coordinator sends a referral, plus the practitioner surface that places one. // // Two gateways, and the split is not a preference. A route answers only on the host whose SAM // template declares its Lambda, and calling the wrong one fails the CORS preflight - which the // browser reports as a CORS error rather than as a missing route. // // THE SPLIT IS BY TEMPLATE, NOT BY TOPIC, and reading it as "referral things go over here" is how // this went wrong once. `/R4/referral-contacts/{npi}` reads like a referral sub-path and is declared // on the OTHER gateway, so the address-book lookup failed as net::ERR_FAILED on every referral, on // a call whose token carried both of its scopes. When adding a route here, grep the templates for // its path rather than matching it to a name. // Declared in 02-fhir-base: /referral/inbound, /referral/{id}/contact, /claim-code, /fax, /link, // /state, /referral/queue. const REFERRAL_API_URL = environment.fhirBaseApi; // Declared in 03-fhir-clinical: /referral/{id}/patient-candidates, /referral/{id}/redeliver, // /referral/{id}/withdraw, ServiceRequest search, create and read-by-id, the referral // DIRECTORY, and the shared referral-contact book at /R4/referral-contacts/{npi}. const SERVICE_REQUEST_API_URL = environment.fhirClinicalApi; /** * How a referral was worked. * * ONE SHAPE FOR EVERY METHOD, mirroring the server's own rule: *"a fax has a number, a call has a * person - but both have a recipient, an outcome, and what was agreed, and the record does not get to * know which one it is looking at."* A per-method shape is how a phone record quietly becomes thinner * than a faxed one, which is the exact failure the logging exists to prevent. */ export interface ReferralPatientCandidate { id: string; name: string; birthDate?: string; } export type ReferralFaxField = | 'patientName' | 'patientBirthDate' | 'patientPhone' | 'patientAddress' | 'patientInsurance' | 'referringProvider' | 'referringOrganization' | 'referringPhone' | 'referringFax' | 'requestedService' | 'reason' | 'diagnosis' | 'urgency'; export interface ReferralFaxLine { page: number; line: number; text: string; confidence: number; } /** One value read off the fax, with the lines that carry it. Nothing here is saved until a person checks it. */ export interface ReferralFaxReadField { value: string; lines: ReferralFaxLine[]; uncertain: boolean; /** The fax does not settle whose value this is; none is offered, only the lines to read. */ unassigned?: true; } /** A prepared link to a received fax's original, as the server issued it for this opening. */ export interface ReferralFaxOriginal { url: string; expiresIn: number; } /** The server answered, but not with a link this client can open; nothing is opened from it. */ export class UnreadableFaxOriginalError extends Error { constructor() { super('The answer for the fax original could not be read.'); this.name = 'UnreadableFaxOriginalError'; } } export type ReferralFaxReadResult = | { state: 'reading' } | { state: 'failed'; message: string } | { state: 'ready'; reading: { fields: Partial>; droppedUngrounded: number; truncated: boolean; }; }; /** The sender and reason a person checked against the fax; the match writes them onto the referral. */ export interface ReferralConfirmedFaxDetails { from?: string; reason?: string; } export type ReferralContactMethod = 'in-system' | 'fax' | 'phone'; export interface ReferralContactAttempt { id?: string; recordedBy?: string; method: ReferralContactMethod; /** Where it went - a fax number, a phone number, or the organization it reached in-system. */ recipient?: string; /** Who answered. A person, and the single most valuable thing on a phone record. */ spokeWith?: string; /** What happened, in the coordinator's own words. */ outcome?: string; occurredAt: string; } export interface ReferralClaimCode { code: string; /** Grouped `XXXX-XXXX` for reading aloud and copying off paper. */ printed: string; createdAt: string; redeemed: boolean; } /** * What redeeming a code answers, and it is deliberately thin. * * `alreadyClaimed` is a SUCCESS, not an error: this practice already holds the referral, so a * coordinator who typed the code twice - or whose colleague got there first - is told it is here rather * than that they did something wrong. * * `arrival` says whether the referral reached the front desk or only the queue. The server reports it * rather than staying silent because "delivered to the desk" and "this practice has not said where * referrals go" need different things from a human. It is absent when the referral was already claimed, * since nothing new arrived. */ /** Routing information about an organisation. Shared with every practice that refers there. */ export interface ReferralSharedRouting { preferredMethod?: 'fax' | 'phone' | 'email' | 'portal' | 'in-system'; fax?: string; phone?: string; email?: string; portalUrl?: string; /** "Mon-Thu 8-4, closed Fridays" - when a human can be reached, not a schedule we act on. */ hours?: string; /** "Referrals go to the referral department" - a role or a desk, never a named relationship. */ routeTo?: string; } export interface ReferralContact { shared?: ReferralSharedRouting & { updatedAt?: string }; /** * THE CALLER'S OWN organisation's note, never another's - the server builds its key from the caller's * org, so there is no request that could return somebody else's. */ note?: string; /** The values as they stood BEFORE each recent change, so a coordinator can undo a bad edit. */ revisions: { changedAt: string; previous: ReferralSharedRouting }[]; } export interface ReferralContactWrite { shared?: ReferralSharedRouting; note?: string; } export interface ReferralClaimRedemption { referralId: string; alreadyClaimed: boolean; arrival?: { destination: string; delivered: boolean }; } /** * - a referral that arrived on paper, as the coordinator can type it. * * THERE IS NO PATIENT FIELD, AND THAT IS THE POINT. Spencer's ruling puts the patient at the point of * CONTACT - found or registered when somebody rings them - not at entry. A required field nobody can * fill while holding a fax is how a queue stays empty and the paper tray survives. */ export interface LoggedInboundReferral { /** The practice it came from. A display string: they are not on the system. */ from: string; reason?: string; note?: string; } export interface LoggedInboundReferralResult { /** * The handle for the whole life of this referral. The queue item is keyed on it, and when a patient * is finally linked the `ServiceRequest` is created AT THIS ID - which is what lets the item leave the * queue by the same route every other referral does. */ referralId: string; queued: boolean; } /** * What a preview answers: the page itself, and whether it could be sent as it stands. * * `claimCode` is absent until the referral has actually been faxed once, because previewing mints * nothing. Absent is the truth, never an error. */ export interface ReferralPacketPreview { contentType: string; filename: string; /** The same PDF the send would transmit, base64 encoded. */ pdfBase64: string; /** False when the packet would not be actionable on arrival - it is still returned. */ actionable: boolean; claimCode?: string; notActionableReason?: string; } export interface ReferralFaxResult { /** The VENDOR accepted the job. Not "the specialist has it" - nothing here can know that. */ accepted: boolean; vendorJobId: string; vendorStatus?: string; pageCount: number; /** The claim code that went on the page, grouped for reading aloud. */ printedCode: string; /** Whether the send was also written to the coordinator's own contact log. */ contactRecorded: boolean; } /** * - what a practitioner supplies when placing a referral. * * NO STATE FIELD, deliberately: the server stamps `sent` and a client must never choose the opening * state. And no `requester` - the server derives it from the caller, which is the only honest source. */ export interface PlaceReferralRequest { patientId: string; /** * The ICD-10-CM diagnosis the referral is for, when the clinician found one. * * OPTIONAL AND BESIDE THE FREE TEXT, never instead of it. A clinician who cannot find a code must * still be able to say why they are referring, and taking that door away while adding this one would * be the same defect in a different field. */ diagnosis?: IValueSet; /** * Who it is going to, as a display string. * * A DISPLAY RATHER THAN A REFERENCE, because the receiving practice is a registry entry and * usually has no Akashic `Organization` to point at. The queue reads a display-only performer as * OUTBOUND, which is what this is. */ toDisplay: string; /** * The recipient's NPI, when the coordinator chose them from the directory. * * WHY IT TRAVELS AT ALL: a display string cannot be looked up. The shared referral-contact book - * the fax number and preferred method one practice learns on the phone and every other practice * inherits - is keyed on NPI, because that is the only stable handle a registry entry has. Without * this the book has no key and stays unreachable. * * ABSENT IS ORDINARY, not a gap. A referral typed in by hand, when the registry cannot answer, has * no NPI and never will. That path must keep working, so nothing may require this. */ toNpi?: string; /** * THE VISIT IT CAME OUT OF - step 4, and the one link that cannot be recovered later. * * *"Ordered at this visit"* is what puts a referral into the visit container that produced it. A * referral placed today without it can never be attributed to that visit afterwards: a referral * placed on the DAY of a visit is not evidence it came out of it, and guessing is the join the * whole record refuses to make. * UNDEFINED IS CORRECT AND COMMON. A referral placed from a chart opened OUTSIDE a visit did not * come out of one, and stamping it would be a lie the visit container would then repeat. */ encounterId?: string; /** Why they are being referred, in the practitioner's words. */ reason?: string; /** Anything else the receiving practice should see. */ note?: string; /** Where and how to reach the recipient, from the directory entry chosen. The patient books with it. */ toContact?: { address?: string; phone?: string }; } const scoped = (scope?: ReferralRequestScope) => (scope ? { admit: scope.admit, signal: scope.signal, organizationId: scope.organizationId, ...(scope.onSend ? { onSend: scope.onSend } : {}) } : {}); export const useReferralWorkbenchService = () => { const { get, post, put } = useHttpService(); /** * Record that a referral was worked, by whichever route. * * IT RECORDS WHAT **WE** DID, NEVER WHAT THE SPECIALIST RECEIVED - the card's third control. A * coordinator who reads "sent" as "they have it" stops chasing, and a referral nobody is chasing is * a patient who does not reach a specialist. The copy around this call has to carry that. */ const recordContact = useCallback( async (referralTaskId: string, attempt: ReferralContactAttempt, scope?: ReferralRequestScope): Promise => { const result = await post<{ attempts?: unknown }>(`${REFERRAL_API_URL}/referral/${referralTaskId}/contact`, attempt, scoped(scope)); return readReferralContacts(result?.attempts); }, [post], ); const readContactHistory = useCallback( async (taskId: string, scope: ReferralRequestScope): Promise => { const result = await get(`${REFERRAL_API_URL}/referral/queue`, scoped(scope)); const matches = result?.referrals?.filter(row => row.taskId === taskId); return matches?.length === 1 ? readReferralContacts(matches[0].contacts) : undefined; }, [get], ); /** * The claim code for a practice that is not on Akashic yet. * * IDEMPOTENT ON THE SERVER: asking twice returns the SAME code while it is still usable, so a * coordinator re-reading it down the phone cannot invalidate the one already on a fax. */ const mintClaimCode = useCallback( async (referralOrderId: string, scope?: ReferralRequestScope): Promise => post(`${REFERRAL_API_URL}/referral/${referralOrderId}/claim-code`, {}, scoped(scope)), [post], ); /** * Redeem a code the sending practice read out, and take delivery of their referral. * * THE OTHER HALF OF `mintClaimCode`, AND IT WAS MISSING. The sending screen has always told a * coordinator "give the receiving practice this code, they enter it when they sign in" - and there was * nowhere to enter it. The route existed and nothing called it. * * THE REFUSALS ARE THE SERVER'S AND ARE NOT REINTERPRETED HERE. Malformed, unknown, already spent, and * the sender trying to claim their own all answer 404 with ONE message, deliberately: telling somebody * "that code is real but spent" confirms they have found a live referral, and the route would become a * way to probe for them. An EXPIRED code is the single exception the server makes - 410, and it says so - * because this route is authenticated and org-scoped, so the person learning it is signed-in staff at an * organization we hold a BAA with, and a coordinator who cannot tell "expired" from "mistyped" retypes a * dead code until they give up. * * -> So the client shows what the server said. It must never add a distinction the server withheld. */ /** * HOW TO REACH A SPECIALIST, as every practice before us learned it on the phone. * * THE SPLIT IS THE WHOLE DESIGN AND IT IS ENFORCED ON THE SERVER, not here. `shared` is routing * information about an organisation - the fax, the preferred method, which desk referrals go to - and * it reaches every practice that ever refers there. `note` is a relationship somebody earned, stays * with the org that wrote it, and is fetched by a key built from the CALLER's organisation so there is * no code path that could return another practice's. * * A SCREEN THAT FILED A RELATIONSHIP UNDER `shared` WOULD PUBLISH IT TO EVERY COMPETITOR of the * practice that earned it. The server checks the field names rather than trusting them, but the UI is * where the mistake would be made, so the two must never be one input. */ const readReferralContact = useCallback( async (npi: string, scope?: ReferralRequestScope): Promise => get(`${SERVICE_REQUEST_API_URL}/referral-contacts/${encodeURIComponent(npi)}`, scoped(scope)), [get], ); /** * Save what the coordinator just learned. Only the fields sent are written - an absent key leaves what * is stored alone, so recording a fax cannot erase the hours somebody else recorded. */ const saveReferralContact = useCallback( async (npi: string, entry: ReferralContactWrite, scope?: ReferralRequestScope): Promise => { await put(`${SERVICE_REQUEST_API_URL}/referral-contacts/${encodeURIComponent(npi)}`, entry, scoped(scope)); }, [put], ); /** * THE SENDER STATES RECEIPT, for a recipient who cannot sign in. * * Spencer, 2026-09-16: a referral to a practice that is not on Akashic is closed by stating they * received it - the fax transmission accepted, or the coordinator marking the call accepted. No * booking here can ever be the evidence, because the practice that would book it is not on Akashic. * * THE SERVER IS STILL THE GATE. It refuses this for any recipient who IS on Akashic, and for a * bare-specialty referral with nobody named - the client only decides whether to OFFER it. */ const markReferralAccepted = useCallback( async (referralOrderId: string, scope?: ReferralRequestScope): Promise => { await put(`${REFERRAL_API_URL}/referral/${referralOrderId}/state`, { state: "claimed" }, scoped(scope)); }, [put], ); // Off-platform booking happens at the other office; the sender records scheduled because no // appointment here can close the loop. const markReferralScheduled = useCallback( async (referralOrderId: string, scope?: ReferralRequestScope): Promise => { await put(`${REFERRAL_API_URL}/referral/${referralOrderId}/state`, { state: "scheduled" }, scoped(scope)); }, [put], ); const redeemClaimCode = useCallback( async (code: string, scope?: ReferralRequestScope): Promise => post(`${REFERRAL_API_URL}/referral-claim/redeem`, { code }, scoped(scope)), [post], ); /** * See the packet before it leaves. * * Ruled 2026-09-14, in the same breath as the refusal to rent a fax number: there was no way to look at * the page without sending it, so nobody could check a patient's demographics, insurance and reason for * referral before they reached an outside practice. A send cannot be recalled. * * The same PDF the send transmits, composed by the same renderer on the server. The client does not * assemble a preview of its own: a page drawn twice is two pages, and the difference would only show on * the one nobody can take back. * * It changes nothing. No claim code is minted, so a referral not yet faxed comes back with no code - * which is the truth rather than a gap. */ const previewReferralPacket = useCallback( async (referralOrderId: string, scope?: ReferralRequestScope): Promise => get(`${REFERRAL_API_URL}/referral/${referralOrderId}/fax-preview`, scoped(scope)), [get], ); /** * Fax the referral packet. * * THE SERVER COMPOSES THE PAGE AND WRITES THE LOG. This sends a destination and the coordinator's * own task id and nothing else - the packet is built from the stored record, and the contact entry is * appended by the code that knows the fax was accepted. A client that faxed and then logged could * half-complete: the page arrives at the specialist and this practice's record says it never went. * * IT DOES NOT MEAN THE SPECIALIST HAS IT. The vendor accepted the job; that is all `accepted` * says, and the copy around this call has to carry that the same way the phone path does. */ const faxReferral = useCallback( async ( referralOrderId: string, request: { toNumber: string; taskId?: string }, scope?: ReferralRequestScope, ): Promise => post(`${REFERRAL_API_URL}/referral/${referralOrderId}/fax`, request, scoped(scope)), [post], ); /** * - PUT A REFERRAL THAT ARRIVED BY FAX OR PHONE INTO THE QUEUE. * * THE GAP IT CLOSES IS THE WRONG WAY ROUND WITHOUT IT: every other arrival path starts with another * Akashic practice minting a claim code, so the referrals that most need tracking - the ones on paper * - had no way in at all. * * NO `ServiceRequest` IS CREATED BY THIS. `subject` is 1..1 in FHIR R4, so a referral about nobody * is not a resource that can exist - what arrives on paper is a WORK ITEM until somebody identifies * the person. The server mints the id that both will share. */ const logInboundReferral = useCallback( async (entry: LoggedInboundReferral, scope?: ReferralRequestScope): Promise => post(`${REFERRAL_API_URL}/referral/inbound`, entry, scoped(scope)), [post], ); /** * - NAME THE PATIENT A FAXED REFERRAL IS ABOUT. * * THIS IS THE MOMENT THE QUEUE ITEM BECOMES A `ServiceRequest`. It could not have been one * before: `subject` is 1..1 in FHIR R4, so a referral about nobody is not a resource that can exist. * * THE SERVER CREATES IT AT THE ID THE ITEM WAS MINTED WITH, which is why this sends only the * patient. A client that created the referral itself and then told the queue about it could * half-complete - a record on a patient's chart and a queue row that never leaves. */ // Scoped to this referral and to patients already affiliated with the practice; the coordinator // has no Patient search of their own. const findReferralPatientCandidates = useCallback( async (referralId: string, term: string, scope?: ReferralRequestScope): Promise => { const response = await get<{ candidates?: ReferralPatientCandidate[] }>( `${SERVICE_REQUEST_API_URL}/referral/${encodeURIComponent(referralId)}/patient-candidates?q=${encodeURIComponent(term)}`, scoped(scope), ); return response?.candidates ?? []; }, [get], ); const linkReferralToPatient = useCallback( async (referralId: string, patientId: string, scope?: ReferralRequestScope, confirmed?: ReferralConfirmedFaxDetails): Promise => { await post(`${REFERRAL_API_URL}/referral/${referralId}/link`, confirmed ? { patientId, confirmed } : { patientId }, scoped(scope)); }, [post], ); // Served by the api stack, so the base is apiUrl. Asking again returns the same reading; 'reading' means ask again shortly. const readReferralFax = useCallback( async (referralId: string, scope?: ReferralRequestScope): Promise => { const result = await post( `${environment.apiUrl}/referral/${encodeURIComponent(referralId)}/fax-read`, {}, scoped(scope), ); return result ?? { state: 'reading' }; }, [post], ); // The server checks the practice, the stored file and the exact version's scan, and audits the open before it // issues a short-lived link. Only an https link of the expected shape is handed on. const openReferralFaxOriginal = useCallback( async (referralId: string, scope?: ReferralRequestScope): Promise => { const answer = await post | undefined>( `${environment.apiUrl}/referral/${encodeURIComponent(referralId)}/fax-document`, { mode: 'preview' }, scoped(scope), ); const { url, expiresIn } = answer ?? {}; if (typeof url !== 'string' || !url.startsWith('https://') || typeof expiresIn !== 'number' || !(expiresIn > 0)) { throw new UnreadableFaxOriginalError(); } return { url, expiresIn }; }, [post], ); /** * - CLOSE A REFERRAL THIS PRACTICE WILL NEVER BOOK, and tell the sender why. * * Spencer ruled (b) on 2026-08-24. The reason exists for the PATIENT's sake, not for tidiness: * *"they declined"* and *"we sent it to the wrong place"* lead the sending practice to do completely * different things for somebody who still needs care, and a bare *"it ended"* leaves them guessing. * * IT TAKES THE ORDER ID, NOT THE TASK ID - unlike `recordContact` next door, and the difference is * the org boundary. A contact attempt is OUR account of OUR work and lives on our Task; the state and * its reason live on the SHARED order, because the sender has to read the same fact we wrote. * * A FIXED CODE, never a sentence. This is the one value here that crosses into another * organization, and free text between practices is a disclosure surface that is much easier to add * later than to take back. The server refuses anything off the list. */ const closeReferral = useCallback( async (referralOrderId: string, reason: ReferralClosureReason, scope?: ReferralRequestScope): Promise => { await put(`${REFERRAL_API_URL}/referral/${referralOrderId}/state`, { state: 'expired', reason }, scoped(scope)); }, [put], ); /** * - A PRACTITIONER PLACES THE REFERRAL. The first step of the whole outbound half, and * until now the only one nothing in the product could do. * * PRACTITIONERS ONLY, AND THAT IS THE RULING RATHER THAN CAUTION. `POST /R4/ServiceRequest` allows * `[PRACTITIONERS]` alone while the queue routes beside it allow Admins, Receptionists and * Practitioners too - *"the practitioner places the order; staff executes. Two actions, two people."* * So this must never be reachable from the coordinator workspace: it would 403 for that * workspace's only audience, and fail at the SCOPE rather than the role, naming the wrong gate. * * THE STATE IS NOT SENT AND MUST NOT BE. The server stamps `sent` itself, because a client that * could choose the opening state could create one already `scheduled` - and the machine only moves * forward, so that is unrecoverable rather than merely wrong. * * `performer` IS DISPLAY-ONLY, and that is correct FHIR rather than a shortcut. The receiving * practice is a registry entry, not an Akashic `Organization` - there is usually no resource to * reference, and FHIR allows a `Reference` carrying only `display`. * * AN EARLIER VERSION OF THIS COMMENT SAID A DISPLAY-ONLY PERFORMER IS WHAT MAKES THE REFERRAL READ * AS OUTBOUND. IT DOES NOT, AND THE MISTAKE COST A WALK. `directionOf` keys on the **requester**, not * the performer, and the requester is stamped by the SERVER as the placing organization - the client * neither sends one nor could be trusted to. Do not add a requester here to "fix" a direction * problem; a client that could name the requester could attribute a referral to another practice. */ const placeReferral = useCallback( async (request: PlaceReferralRequest): Promise<{ id?: string }> => /* * The patient goes in the QUERY STRING as well as the body, and that is not redundancy. * This route is gated on patient access, and the gate runs in an API Gateway request * authorizer, which never receives a body - it resolves the patient from a path id or a * query parameter and refuses when it finds neither. Without this the call is denied * before any handler sees it, and the log line is "No patient ID found for access * validation" while the browser is told only that it is not authorized. * * Named `subject` to match the field it duplicates. Every other collection POST in this * client does the same with `patient`; the validator accepts patient, subject or for. */ post<{ id?: string }>( `${SERVICE_REQUEST_API_URL}/ServiceRequest?subject=${encodeURIComponent(`Patient/${request.patientId}`)}`, { resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: `Patient/${request.patientId}` }, /* * THE IDENTIFIER TRAVELS BESIDE THE DISPLAY, which is what FHIR's Reference is for: an * organization we hold no resource for, named by its business identifier. The api validates * nothing about performer and stores what it is given, so this needs no server change. * * It is what makes the recipient LOOKABLE-UP later. A display string identifies nobody - * two clinics share a name, one clinic is spelled three ways - and the referral-contact * book is keyed on NPI precisely because of that. */ performer: [{ display: request.toDisplay, ...recipientContactExtension(request.toContact), ...(request.toNpi ? { identifier: { system: 'http://hl7.org/fhir/sid/us-npi', value: request.toNpi, }, } : {}), }], // step 4. Only when the caller was in a visit - see `encounterId`. ...(request.encounterId ? { encounter: { reference: `Encounter/${request.encounterId}` } } : {}), // ONE reasonCode CARRYING BOTH, and this is the same shape the diagnostic order // writes: the coding for anything downstream that matches on it, the text for the // human who typed it, and the field absent entirely when neither was given. // // `reasonCode` was a CodeableConcept all along. Writing `.text` and leaving // `.coding` empty was never the design - it is the shape the brief rules out, free // text standing INSTEAD of a code rather than beside it. ...(request.diagnosis || request.reason ? { reasonCode: [ { ...(request.diagnosis ? { coding: [{ system: request.diagnosis.system, code: request.diagnosis.code, display: request.diagnosis.display, }], } : {}), ...(request.reason ? { text: request.reason } : {}), }, ], } : {}), ...(request.note ? { note: [{ text: request.note }] } : {}), }, ), [post], ); /** * The referrals already placed for one patient. Subject-scoped rather than read off the coordinator * queue, because the chart needs the orders themselves rather than the queue's projection. * * Paginated because this returns every ServiceRequest for the patient, lab and imaging included, * and the caller filters to referrals afterwards. A page cut therefore does not merely shorten the * list: a long order history can push every referral past the boundary, so the chart would show no * referrals while the patient has them. */ const listPatientReferrals = useCallback( async (patientId: string): Promise> => fetchAllPagesAsBundle((pageParam) => { const params = new URLSearchParams(); params.append('_count', searchPageSize('100')); params.append('subject', `Patient/${patientId}`); if (pageParam) params.set('page', pageParam); return get>( `${SERVICE_REQUEST_API_URL}/ServiceRequest?${params.toString()}`, ); }), [get], ); /** * One patient's rows from the coordinator queue, narrowed by the server after its org fence. It * answers for a patient whose clinical ServiceRequest read is refused, as an affiliation-only * patient's is. `incomplete` means a referral may be missing, not that there are none. */ const readPatientReferralQueue = useCallback( async ( patientId: string, organizationId: string | undefined, ): Promise<{ rows: ReferralQueueRow[]; incomplete: boolean }> => { // The caller's own practice, never the transport's mutable default, and none means no read. if (!organizationId) throw new Error('A practice is required to read its referral queue'); const response = await get( `${REFERRAL_API_URL}/referral/queue?patient=${encodeURIComponent(patientId)}`, { organizationId }, ); return { rows: (response?.referrals ?? []).map(toRow), // A short read or an unreadable order: either way a referral may be missing. incomplete: response?.truncated === true || response?.unreadable === true, }; }, [get], ); /** * One referral, by its own id. * * THIS EXISTS BECAUSE A SEARCH CANNOT SEE A RECORD IT HAS NOT INDEXED YET, AND A READ BY ID CAN. * `listPatientReferrals` above is a SEARCH, and HealthLake's search index lags a write by minutes - * measured at two and a half on 2026-08-31 from the handler's own logs, with the resource present * and findable by id the whole time. So a chart that re-runs the search immediately after placing a * referral is asking the one question that cannot yet answer it, and the practitioner is told the * referral was placed and then shown a list without it. * * The fix is NOT to poll: the wait is minutes, so retrying the search only lengthens the blank * window. It is to ask the question that has an answer. */ /** The patient says they booked a referral whose practice is not on Akashic. */ const markReferralBooked = useCallback( async (orderId: string): Promise<{ id: string; patientBookedAt: string }> => put<{ id: string; patientBookedAt: string }>( `${SERVICE_REQUEST_API_URL}/ServiceRequest/${orderId}/patient-booked`, {}, ), [put], ); /** * Finish delivering a referral routed to a practice on Akashic whose delivery failed or never confirmed. * The same order is delivered again - the server keeps any queue row already written - so a retry never * makes a second referral. Answers with how delivery went this time. */ const redeliverReferral = useCallback( async (orderId: string, scope?: ReferralRequestScope): Promise<{ delivery: 'pending' | 'delivered' | 'failed' }> => post<{ delivery: 'pending' | 'delivered' | 'failed' }>( `${SERVICE_REQUEST_API_URL}/referral/${encodeURIComponent(orderId)}/redeliver`, {}, scoped(scope), ), [post], ); /** * The sender withdraws a referral placed in error (referrals.html#s-withdraw-referral). The server keeps the * order and records who and why, stops any pending Akashic delivery or claim, and tells a receiver that had * already claimed it. It cannot recall a copy already received outside Akashic, and nothing here says it can. */ const withdrawReferral = useCallback( async (orderId: string, reason: 'wrong-practice' | 'placed-in-error' | 'other', scope?: ReferralRequestScope): Promise<{ externalCopyPossible?: boolean }> => post<{ externalCopyPossible?: boolean }>( `${SERVICE_REQUEST_API_URL}/referral/${encodeURIComponent(orderId)}/withdraw`, { reason }, scoped(scope), ), [post], ); const declineReferralInvite = useCallback( async (orderId: string): Promise<{ id: string; inviteDeclinedAt: string }> => put<{ id: string; inviteDeclinedAt: string }>( `${SERVICE_REQUEST_API_URL}/ServiceRequest/${orderId}/invite-declined`, {}, ), [put], ); const readPlacedReferral = useCallback( async (orderId: string): Promise => get(`${SERVICE_REQUEST_API_URL}/ServiceRequest/${orderId}`), [get], ); return { recordContact, readContactHistory, mintClaimCode, redeemClaimCode, previewReferralPacket, faxReferral, logInboundReferral, findReferralPatientCandidates, linkReferralToPatient, readReferralFax, openReferralFaxOriginal, closeReferral, markReferralAccepted, markReferralScheduled, placeReferral, listPatientReferrals, markReferralBooked, redeliverReferral, withdrawReferral, declineReferralInvite, readPatientReferralQueue, readPlacedReferral, readReferralContact, saveReferralContact, }; };