feat(frontend): ClaimDetail type + api.getClaimDetail

This commit is contained in:
Tyler
2026-06-20 10:01:40 -06:00
parent b984c01d9a
commit 44e1c4616d
3 changed files with 256 additions and 1 deletions
+106 -1
View File
@@ -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);
});
});