// --------------------------------------------------------------------------- // Inbox API client (sub-project 6). // // Mirrors the four-lane working surface: rejected / candidates / unmatched / // done_today. Plus the actions: manual match, dismiss, resubmit, export. // All non-2xx responses throw (so callers can branch on status). // --------------------------------------------------------------------------- export type ScoreBreakdown = { patient: number; date: number; amount: number; provider: number; }; export type ScoreTier = "strong" | "weak" | "hidden"; export type InboxClaimRow = { id: string; kind: "claim"; patient_control_number: string; charge_amount: number | null; payer_id: string | null; provider_npi: string | null; state: string; rejection_reason: string | null; rejected_at: string | null; service_date_from: string | null; score?: number | null; score_tier?: ScoreTier | null; score_breakdown?: ScoreBreakdown | null; /** * SP7: populated on `rejected` / `done_today` claim rows when the claim * has a matched remittance. Carries the line-count badge the * `MatchedRemitCard` displays (``3/4 matched lines``). */ matched_remittance?: { id: string; matched_lines: number; total_lines: number; } | null; }; export type InboxCandidateRow = { id: string; kind: "remit"; payer_claim_control_number: string; charge_amount: number | null; payer_id: string | null; rendering_provider_npi: string | null; service_date: string | null; candidates: Array<{ claim_id: string; score: number; tier: ScoreTier; breakdown: ScoreBreakdown; }>; }; export type InboxLanes = { rejected: InboxClaimRow[]; candidates: InboxCandidateRow[]; unmatched: InboxClaimRow[]; done_today: InboxClaimRow[]; }; const BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? ""; function joinUrl(path: string): string { return `${BASE_URL.replace(/\/$/, "")}${path}`; } async function getJson(path: string): Promise { const r = await fetch(joinUrl(path)); if (!r.ok) { throw new Error(`GET ${path} failed: ${r.status}`); } return (await r.json()) as T; } async function postJson(path: string, body: unknown): Promise { const r = await fetch(joinUrl(path), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) { throw new Error(`POST ${path} failed: ${r.status}`); } return (await r.json()) as T; } export async function fetchInboxLanes(): Promise { return getJson("/api/inbox/lanes"); } export async function matchCandidate( remitId: string, claimId: string, ): Promise { await postJson(`/api/inbox/candidates/${encodeURIComponent(remitId)}/match`, { claim_id: claimId, }); } export async function dismissCandidates( pairs: Array<{ claim_id: string; remit_id: string }>, ): Promise { await postJson("/api/inbox/candidates/dismiss", { pairs }); } export type ResubmitResult = { ok: boolean; resubmitted: string[]; conflicts: Array<{ claim_id: string; current_state: string }>; }; export async function resubmitRejected( claimIds: string[], ): Promise { return postJson("/api/inbox/rejected/resubmit", { claim_ids: claimIds, }); } export function exportInboxCsvUrl( lane: "rejected" | "candidates" | "unmatched" | "done_today", ): string { return joinUrl(`/api/inbox/export.csv?lane=${lane}`); }