feat(frontend): ClaimDetail type + api.getClaimDetail
This commit is contained in:
+106
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { api } from "./api";
|
||||
import { ApiError, api } from "./api";
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
const mockFetch = vi.fn();
|
||||
@@ -70,4 +70,109 @@ describe("api GET helpers", () => {
|
||||
const called = mockFetch.mock.calls[0][0] as string;
|
||||
expect(called).toContain("/api/batches/abc");
|
||||
});
|
||||
|
||||
it("getClaimDetail hits /api/claims/{id} and returns the parsed body", async () => {
|
||||
// Minimal valid ClaimDetail shape — every spec-mandated key present,
|
||||
// including the optional `matchedRemittance: null` for an unpaired
|
||||
// claim (the dominant UI render path).
|
||||
const body = {
|
||||
id: "CLM-1",
|
||||
batchId: "B-1",
|
||||
state: "submitted",
|
||||
stateLabel: "Submitted",
|
||||
billedAmount: 123.45,
|
||||
patientName: "Jane Doe",
|
||||
providerNpi: "1234567890",
|
||||
providerName: "Acme Clinic",
|
||||
payerName: "Medicaid",
|
||||
payerId: "MEDCO",
|
||||
submissionDate: "2026-01-15T00:00:00Z",
|
||||
serviceDateFrom: "2026-01-10",
|
||||
serviceDateTo: "2026-01-10",
|
||||
parsedAt: "2026-01-15T00:00:01Z",
|
||||
diagnoses: [{ code: "E11.9", qualifier: "ABK" }],
|
||||
serviceLines: [
|
||||
{
|
||||
lineNumber: 1,
|
||||
procedureQualifier: "HC",
|
||||
procedureCode: "99213",
|
||||
modifiers: [],
|
||||
charge: 100.0,
|
||||
units: 1,
|
||||
unitType: "UN",
|
||||
serviceDate: "2026-01-10",
|
||||
},
|
||||
],
|
||||
parties: {
|
||||
billingProvider: {
|
||||
name: "Acme Clinic",
|
||||
npi: "1234567890",
|
||||
taxId: "12-3456789",
|
||||
address: {
|
||||
line1: "123 Main St",
|
||||
line2: null,
|
||||
city: "Denver",
|
||||
state: "CO",
|
||||
zip: "80202",
|
||||
},
|
||||
},
|
||||
subscriber: {
|
||||
firstName: "Jane",
|
||||
lastName: "Doe",
|
||||
memberId: "M-1",
|
||||
dob: null,
|
||||
gender: "F",
|
||||
},
|
||||
payer: { name: "Medicaid", id: "MEDCO" },
|
||||
},
|
||||
validation: { passed: true, errors: [], warnings: [] },
|
||||
rawSegments: [["ISA*00*"]],
|
||||
matchedRemittance: null,
|
||||
stateHistory: [
|
||||
{
|
||||
kind: "claim_submitted",
|
||||
ts: "2026-01-15T00:00:00Z",
|
||||
batchId: "B-1",
|
||||
remittanceId: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})
|
||||
);
|
||||
const out = await api.getClaimDetail("CLM-1");
|
||||
expect(out).toEqual(body);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
const called = mockFetch.mock.calls[0][0] as string;
|
||||
// URL must include the encoded id, no trailing query string.
|
||||
expect(called).toContain("/api/claims/CLM-1");
|
||||
expect(called).not.toContain("?");
|
||||
});
|
||||
|
||||
it("getClaimDetail throws an ApiError on 404", async () => {
|
||||
// The backend raises `{"error":"Not found","detail":"Claim X not found"}`
|
||||
// on missing ids (see api.py:get_claim_detail_endpoint). The client
|
||||
// must surface it as an ApiError so the drawer can distinguish
|
||||
// "doesn't exist" from a transient fetch failure (spec §3.4).
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({ error: "Not found", detail: "Claim ghost not found" }),
|
||||
{
|
||||
status: 404,
|
||||
headers: { "content-type": "application/json" },
|
||||
}
|
||||
)
|
||||
);
|
||||
let caught: unknown;
|
||||
try {
|
||||
await api.getClaimDetail("ghost");
|
||||
} catch (err) {
|
||||
caught = err;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(ApiError);
|
||||
expect((caught as ApiError).status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
import type {
|
||||
Ack,
|
||||
ClaimDetail,
|
||||
ClaimOutput,
|
||||
ClaimPayment,
|
||||
Envelope,
|
||||
@@ -451,6 +452,29 @@ async function listClaims<T = unknown>(
|
||||
return (await res.json()) as PaginatedResponse<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one claim with the full SP4 detail-drawer context (header, state,
|
||||
* parties, validation, service lines, diagnoses, raw segments, recent
|
||||
* ``stateHistory``, and a populated ``matchedRemittance`` when paired).
|
||||
*
|
||||
* Drives ``GET /api/claims/{claim_id}``. Throws ``ApiError`` on non-2xx —
|
||||
* including 404, which the spec §3.4 calls out as a distinct "claim
|
||||
* doesn't exist" state in the drawer (separate from a transient fetch
|
||||
* failure). Callers should branch on ``error.status === 404`` to render
|
||||
* the not-found state instead of the generic error toast.
|
||||
*/
|
||||
async function getClaimDetail(id: string): Promise<ClaimDetail> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(joinUrl(`/api/claims/${encodeURIComponent(id)}`), {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
return (await res.json()) as ClaimDetail;
|
||||
}
|
||||
|
||||
async function listRemittances<T = unknown>(
|
||||
params: ListRemittancesParams
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
@@ -649,6 +673,7 @@ export const api = {
|
||||
listBatches,
|
||||
getBatch,
|
||||
listClaims,
|
||||
getClaimDetail,
|
||||
listRemittances,
|
||||
getRemittance,
|
||||
listProviders,
|
||||
|
||||
Reference in New Issue
Block a user