import { ReactNode, useCallback, createContext, useContext } from 'react'; import { Bundle, Resource } from 'fhir/r4'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; const API_URL = environment.fhirBaseApi; /** One family the capture could not read in full, and why. */ export interface AnchorReadGap { resourceType: string; reason: string } /** Whether the capture read every chart family; an anchor from before this was recorded says not-recorded. */ export type AnchorCaptureCompleteness = | { state: 'not-recorded' } | { state: 'complete' } | { state: 'incomplete'; gaps: AnchorReadGap[] } | { state: 'unreadable' }; /** * Where the capture stands in time. A reconstruction states the provider instant of the visit's start, the start * edition, what it cannot prove, and whether the agreements were kept with it. Not recorded makes no time claim. */ export type AnchorTemporal = | { state: 'not-recorded' } | { state: 'unreadable' } | { state: 'recorded'; temporal: { kind: 'start-boundary-reconstruction'; boundary: string; edition: string; qualifications: string[]; basisStatus: 'known' | 'none' | 'unavailable'; }; }; export interface AnchorResolveResult { anchor: { id: string; subject?: string; encounter?: string; date?: string; source?: string; hash?: string; previousAnchorId?: string; captureTime?: string; resourceCount?: number; }; hashVerified: boolean; /** Absent from a server older than these fields. */ captureCompleteness?: AnchorCaptureCompleteness; temporal?: AnchorTemporal; resolutionIncomplete?: boolean; resolvedResources: Resource[]; unresolvedRefs: string[]; } export interface ExportCareViewAnchorOptions { includePointInTime: boolean; asOf?: string; } /** * Thrown when the server's B3 export endpoint blocks a legal export because the anchor failed its * own hash/integrity verification (HTTP 409, body `{ code: 'anchor-tampered' }`). Court-grade * guarantee: never treat a tampered anchor's export failure as a generic error the caller might * silently retry past - callers should catch this specifically and refuse to proceed. */ export class AnchorTamperedError extends Error { readonly code = 'anchor-tampered' as const; constructor(message: string = 'Care-view anchor failed integrity verification') { super(message); this.name = 'AnchorTamperedError'; } } export const useCareViewAnchorService = () => { const { get, fetchWithAuth } = useHttpService(); const resolveCareViewAnchor = useCallback((anchorId: string): Promise => { return get(`${API_URL}/CareViewAnchor/${anchorId}/resolve`); }, [get]); // Uses fetchWithAuth directly (not the generic post() helper) because a 409 anchor-tampered // response must be distinguished from any other non-2xx failure and re-thrown as a typed error // - post()'s shared handleErrorResponse collapses every failure into a generic Error and would // lose the status/code needed to tell them apart. Auth + org-header wiring is identical to // resolveCareViewAnchor: same useHttpService, same fhirBaseApi base, no bypassOrganization. const exportCareViewAnchor = useCallback(async ( anchorId: string, opts: ExportCareViewAnchorOptions ): Promise => { const response = await fetchWithAuth(`${API_URL}/CareViewAnchor/${anchorId}/export`, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ includePointInTime: opts.includePointInTime, asOf: opts.asOf }) }); if (!response.ok) { let body: any = null; try { body = await response.json(); } catch { // Non-JSON error body — fall through to the generic error below. } if (response.status === 409 && body?.code === 'anchor-tampered') { throw new AnchorTamperedError(body?.message); } throw new Error(body?.message ?? `Request failed: ${response.status} ${response.statusText}`); } return response.json(); }, [fetchWithAuth]); return { resolveCareViewAnchor, exportCareViewAnchor }; }; const CareViewAnchorServiceContext = createContext | undefined>(undefined); export const CareViewAnchorServiceProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const careViewAnchorService = useCareViewAnchorService(); return ( {children} ); }; export const useCareViewAnchorServiceContext = () => { const context = useContext(CareViewAnchorServiceContext); if (context === undefined) { throw new Error('useCareViewAnchorServiceContext must be used within a CareViewAnchorServiceProvider'); } return context; };