feat(frontend): add listUnmatched, matchRemit, unmatchClaim to api

This commit is contained in:
Tyler
2026-06-19 23:47:34 -06:00
parent d4a142aa83
commit 23ac7f6fda
+73
View File
@@ -17,6 +17,8 @@
* - `health()` GETs `/api/health` and returns `{ status, version }`. * - `health()` GETs `/api/health` and returns `{ status, version }`.
* - `listBatches / getBatch / listClaims / listRemittances / listProviders * - `listBatches / getBatch / listClaims / listRemittances / listProviders
* / listActivity` are plain JSON GETs against the persistence surface. * / listActivity` are plain JSON GETs against the persistence surface.
* - `listUnmatched / matchRemit / unmatchClaim` hit the reconciliation
* surface. POSTs throw `ApiError` so callers can branch on `.status`.
*/ */
import type { import type {
@@ -24,6 +26,7 @@ import type {
ClaimPayment, ClaimPayment,
Envelope, Envelope,
FinancialInfo, FinancialInfo,
MatchResponse,
NdjsonEvent, NdjsonEvent,
ParseResult837, ParseResult837,
ParseResult835, ParseResult835,
@@ -31,6 +34,8 @@ import type {
Payer835, Payer835,
Payee835, Payee835,
ReassociationTrace, ReassociationTrace,
UnmatchedClaim,
UnmatchedResponse,
BatchSummary as ParserBatchSummary, BatchSummary as ParserBatchSummary,
} from "@/types"; } from "@/types";
@@ -147,6 +152,16 @@ function notConfiguredError(): Error {
); );
} }
/**
* Error thrown for non-2xx responses. Carries the HTTP `status` so callers can
* branch on it (e.g. 404 vs 500) without parsing the message string.
*/
export class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
function joinUrl(path: string): string { function joinUrl(path: string): string {
return `${BASE_URL.replace(/\/$/, "")}${path}`; return `${BASE_URL.replace(/\/$/, "")}${path}`;
} }
@@ -436,6 +451,61 @@ async function listActivity<T = unknown>(
return (await res.json()) as PaginatedResponse<T>; return (await res.json()) as PaginatedResponse<T>;
} }
// ---------------------------------------------------------------------------
// Public surface — reconciliation endpoints (sub-project 2)
// POSTs throw `ApiError` so callers can inspect `.status`; the GET is shaped
// like the other list endpoints above.
// ---------------------------------------------------------------------------
async function listUnmatched(): Promise<UnmatchedResponse> {
if (!isConfigured) throw notConfiguredError();
const res = await fetch(joinUrl(`/api/reconciliation/unmatched`), {
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 UnmatchedResponse;
}
async function matchRemit(
claimId: string,
remitId: string
): Promise<MatchResponse> {
if (!isConfigured) throw notConfiguredError();
const res = await fetch(joinUrl(`/api/reconciliation/match`), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ claim_id: claimId, remit_id: remitId }),
});
if (!res.ok) {
const detail = await readErrorBody(res);
throw new ApiError(res.status, detail || res.statusText);
}
return (await res.json()) as MatchResponse;
}
async function unmatchClaim(claimId: string): Promise<{ claim: UnmatchedClaim }> {
if (!isConfigured) throw notConfiguredError();
const res = await fetch(joinUrl(`/api/reconciliation/unmatch`), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ claim_id: claimId }),
});
if (!res.ok) {
const detail = await readErrorBody(res);
throw new ApiError(res.status, detail || res.statusText);
}
return (await res.json()) as { claim: UnmatchedClaim };
}
export const api = { export const api = {
isConfigured, isConfigured,
baseUrl: BASE_URL, baseUrl: BASE_URL,
@@ -448,4 +518,7 @@ export const api = {
listRemittances, listRemittances,
listProviders, listProviders,
listActivity, listActivity,
listUnmatched,
matchRemit,
unmatchClaim,
}; };