feat(sp8): Claim drawer Download 837 button
Three pieces:
- src/lib/download.ts: generic downloadTextFile(filename, mime, text)
helper. Mirrors csv.ts:downloadCsv but takes an explicit MIME type and
drops the BOM prepend (which would corrupt the ISA segment).
- src/lib/api.ts: serializeClaim837(id) → {text, filename}. Fetches
GET /api/claims/{id}/serialize-837, pulls the suggested filename from
Content-Disposition (falls back to claim-{id}.x12 if the header is
missing). Throws ApiError on non-2xx so callers can branch on .status.
- ClaimDrawerHeader: Download icon button between the amount and the
close button. Click → api.serializeClaim837 → downloadTextFile.
Disabled + 'Downloading 837 file' aria-label while the fetch is in
flight so the click feels responsive. Optional onError prop surfaces
fetch failures; defaults to a no-op so existing callers stay clean.
Tests: 3 download.test.ts, 3 api.test.ts, 2 header.test.ts (happy
path + error path). Frontend: 350 passing (+8 from 342).
This commit is contained in:
@@ -175,4 +175,46 @@ describe("api GET helpers", () => {
|
||||
expect(caught).toBeInstanceOf(ApiError);
|
||||
expect((caught as ApiError).status).toBe(404);
|
||||
});
|
||||
|
||||
it("serializeClaim837 returns the regenerated text + filename from Content-Disposition", async () => {
|
||||
const x12 = "ISA*00* *00* *ZZ*CYCLONE *ZZ*RECEIVER *260620*1200*^*00501*000000001*0*P*:~IEA*0*000000001~";
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response(x12, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/x12",
|
||||
// Backend emits: attachment; filename="claim-{id}.x12"
|
||||
"content-disposition": 'attachment; filename="claim-CLM-1.x12"',
|
||||
},
|
||||
})
|
||||
);
|
||||
const out = await api.serializeClaim837("CLM-1");
|
||||
expect(out.text).toBe(x12);
|
||||
expect(out.filename).toBe("claim-CLM-1.x12");
|
||||
// URL must hit the new SP8 endpoint with the encoded id.
|
||||
const called = mockFetch.mock.calls[0][0] as string;
|
||||
expect(called).toContain("/api/claims/CLM-1/serialize-837");
|
||||
// No Accept header negotiation needed — backend always returns text/x12.
|
||||
const init = mockFetch.mock.calls[0][1] as RequestInit | undefined;
|
||||
const headers = (init?.headers ?? {}) as Record<string, string>;
|
||||
expect(headers.Accept).toBeUndefined();
|
||||
});
|
||||
|
||||
it("serializeClaim837 falls back to a default filename if Content-Disposition is missing", async () => {
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response("ISA*00*~", { status: 200 })
|
||||
);
|
||||
const out = await api.serializeClaim837("CLM-2");
|
||||
expect(out.filename).toBe("claim-CLM-2.x12");
|
||||
});
|
||||
|
||||
it("serializeClaim837 throws ApiError on 404", async () => {
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({ error: "Not found", detail: "Claim ghost not found" }),
|
||||
{ status: 404, headers: { "content-type": "application/json" } }
|
||||
)
|
||||
);
|
||||
await expect(api.serializeClaim837("ghost")).rejects.toBeInstanceOf(ApiError);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user