import { useCallback } from 'react'; import { environment } from '../environments/environment'; import { HttpError, useHttpService } from '../services/HttpService.tsx'; // THE REFERRAL DIRECTORY -, the client half. // // Design: `akashic-docs/_CLAUDE/2026-08-21-referral-design-ruling.md` Addendum 4 and Addendum 7 ยง1. // The server half documents why org-first is the direction that works and what the registry's two hard // limits are; this file only has to keep the client from undoing either. // // NOT A TYPE-AHEAD, AND THAT IS A CONSTRAINT RATHER THAN A STYLE CHOICE. The registry publishes no // rate limit anywhere we could find - its API help page is a client-rendered SPA and the responses carry // no ratelimit header of any kind. An unknown limit means ONE CALL PER SUBMITTED SEARCH. A per-keystroke // search would multiply our call volume by roughly the length of a hospital's name against a registry we // cannot measure our budget against, and it is a public service we do not want to be rude to. // // These functions never throw. The screen has three states - results, "nothing yet", and "the registry // is not answering" - and the third is a NORMAL state of this workflow rather than an error, because the // referral is being sent either way. const API_URL = environment.fhirClinicalApi; export interface DirectoryLocation { street: string; street2?: string; city: string; state: string; postalCode: string; phone?: string; fax?: string; } export interface DirectorySpecialty { description: string; /** * NOT A LICENCE CHECK. The registry is an IDENTIFIER registry: an NPI is not revoked when a licence * lapses. A listing proves *registered*, never *currently licensed*, and the screen says so out loud. */ license?: string; licenseState?: string; primary: boolean; } export interface DirectoryOrganization { npi: string; name: string; /** The registry's "doing business as" names - the legal name is often not the common one. */ alsoKnownAs: string[]; location?: DirectoryLocation; specialties: DirectorySpecialty[]; /** * ONLY EVER PRESENT AND `true`. Absent means WE DO NOT KNOW - never "not on Akashic". The server * never sends `false` for exactly this reason, and no screen may infer one. */ onAkashic?: true; /** * WHETHER A REFERRAL SENT THROUGH AKASHIC WOULD LAND SOMEWHERE THEY WATCH. A NARROWER CLAIM THAN * `onAkashic`, AND THE ONE A SENDER ACTUALLY NEEDS. * * A practice can hold an Akashic account and have designated no destination for referrals. Reading * `onAkashic` as permission to send in-system is what the routing policy's missing default exists to * prevent: a referral that lands in a queue nobody watches is worse than a fax, because the sender * believes it arrived. * * Only ever present and `true`, exactly like the flag above - but the two are read TOGETHER, and the * pair is what makes the honest distinction possible: * * onAkashic + acceptsReferrals they take referrals here; send it in-system * onAkashic alone they have an account and have not turned this on; fax or phone * neither we cannot see them - which says NOTHING about whether they exist * * The middle row is a MEASUREMENT, not an inference from absence: the server found the organization * and read its routing. The bottom row remains the one nothing may be concluded from. */ acceptsReferrals?: true; } export interface DirectoryPractitioner { npi: string; name: string; credential?: string; location?: DirectoryLocation; specialties: DirectorySpecialty[]; /** Labelled, never merged - "nearby" is a different claim from "at this address". */ proximity: 'exact-street' | 'nearby'; } export type DirectoryResult = | { available: true; results: T[]; page: number; hasMore: boolean; /** There is more and the registry will not serve it the search must be NARROWED, not paged. */ pagingExhausted: boolean; } | { available: false; reason: 'unavailable' | 'query-too-short' | 'refused' }; export interface OrganizationQuery { name: string; state?: string; city?: string; page?: number; } export interface PractitionerQuery { postalCode: string; street?: string; specialty?: string; state?: string; page?: number; } const paramsOf = (entries: Record): URLSearchParams => { const params = new URLSearchParams(); for (const [key, value] of Object.entries(entries)) { const text = String(value ?? '').trim(); if (text) params.set(key, text); } return params; }; /** * A 400 IS NOT AN OUTAGE. The server separates "the registry is not answering" from "this caller sent * a query that cannot be searched", and collapsing them here would hide a broken screen behind *"the * registry is having a moment"* - the reader would wait for a service that is working fine. */ const asFailure = (error: unknown): DirectoryResult => { if (error instanceof HttpError && error.status === 400) { return { available: false, reason: 'query-too-short' }; } // A refusal is a permission answer, not an outage; waiting for the registry would never help. if (error instanceof HttpError && (error.status === 401 || error.status === 403)) { return { available: false, reason: 'refused' }; } return { available: false, reason: 'unavailable' }; }; export const useReferralDirectory = () => { const { get } = useHttpService(); const searchOrganizations = useCallback(async ( query: OrganizationQuery, ): Promise> => { const params = paramsOf({ name: query.name, state: query.state, city: query.city, page: query.page, }); try { return await get>( `${API_URL}/referral-directory/organizations?${params.toString()}`, ); } catch (error) { console.error('Referral directory: organization search failed', error); return asFailure(error); } }, [get]); const searchPractitioners = useCallback(async ( query: PractitionerQuery, ): Promise> => { const params = paramsOf({ postalCode: query.postalCode, street: query.street, specialty: query.specialty, state: query.state, page: query.page, }); try { return await get>( `${API_URL}/referral-directory/practitioners?${params.toString()}`, ); } catch (error) { console.error('Referral directory: practitioner search failed', error); return asFailure(error); } }, [get]); return { searchOrganizations, searchPractitioners }; }; export default useReferralDirectory;