feat(parsers+api+ui+db): 999 ACK transaction set end-to-end (SP3 P3)

This commit is contained in:
Tyler
2026-06-20 08:03:37 -06:00
parent 7a20f732f2
commit fb2a98fc7a
24 changed files with 2618 additions and 1 deletions
+120
View File
@@ -22,6 +22,7 @@
*/
import type {
Ack,
ClaimOutput,
ClaimPayment,
Envelope,
@@ -334,6 +335,56 @@ async function parse835(
return (await res.json()) as ParseResult835;
}
/**
* Upload a 999 ACK file for parsing. The backend persists the row
* and returns both the parsed result and the persisted ack metadata
* (including the regenerated raw 999 text for download).
*/
async function parse999(
file: File
): Promise<{ ack: Ack & { raw_999_text: string }; parsed: unknown }> {
if (!isConfigured) throw notConfiguredError();
const form = new FormData();
form.append("file", file, file.name);
const res = await fetch(joinUrl("/api/parse-999"), {
method: "POST",
body: form,
headers: { Accept: "application/json" },
});
if (!res.ok) {
const detail = await readErrorBody(res);
throw new ApiError(res.status, detail || res.statusText);
}
const body = (await res.json()) as {
ack: {
id: number;
accepted_count: number;
rejected_count: number;
received_count: number;
ack_code: "A" | "E" | "R" | "P";
source_batch_id: string;
raw_999_text: string;
};
parsed: unknown;
};
return {
ack: {
id: body.ack.id,
sourceBatchId: body.ack.source_batch_id,
acceptedCount: body.ack.accepted_count,
rejectedCount: body.ack.rejected_count,
receivedCount: body.ack.received_count,
ackCode: body.ack.ack_code,
parsedAt: "",
// raw_999_text is appended below (not part of the canonical Ack
// type) so the UI can trigger a download without a second
// round-trip to /api/acks/{id}.
...({ raw_999_text: body.ack.raw_999_text } as { raw_999_text: string }),
} as Ack & { raw_999_text: string },
parsed: body.parsed,
};
}
async function health(): Promise<HealthResponse | null> {
if (!isConfigured) return null;
const res = await fetch(joinUrl("/api/health"));
@@ -522,12 +573,79 @@ async function unmatchClaim(claimId: string): Promise<{ claim: UnmatchedClaim }>
return (await res.json()) as { claim: UnmatchedClaim };
}
// ---------------------------------------------------------------------------
// Public surface — 999 ACKs (SP3 P3 T16)
// Re-shapes the snake_case backend response into the camelCase `Ack` shape
// used throughout the UI.
// ---------------------------------------------------------------------------
/**
* Raw snake_case row from `GET /api/acks`. Re-shaped by `mapAck`
* before the UI sees it.
*/
interface RawAckRow {
id: number;
source_batch_id: string;
accepted_count: number;
rejected_count: number;
received_count: number;
ack_code: "A" | "E" | "R" | "P";
parsed_at: string;
}
function mapAck(row: RawAckRow): Ack {
return {
id: row.id,
sourceBatchId: row.source_batch_id,
acceptedCount: row.accepted_count,
rejectedCount: row.rejected_count,
receivedCount: row.received_count,
ackCode: row.ack_code,
parsedAt: row.parsed_at,
};
}
async function listAcks(params: { limit?: number } = {}): Promise<PaginatedResponse<Ack>> {
if (!isConfigured) throw notConfiguredError();
const query: Record<string, unknown> = {};
if (params.limit !== undefined) query.limit = params.limit;
const res = await fetch(
joinUrl(`/api/acks${qs(query)}`),
{ headers: { Accept: "application/json" } }
);
if (!res.ok) {
const detail = await readErrorBody(res);
throw new ApiError(res.status, detail || res.statusText);
}
const body = (await res.json()) as { items: RawAckRow[]; total: number; returned: number; has_more: boolean };
return {
items: body.items.map(mapAck),
total: body.total,
returned: body.returned,
has_more: body.has_more,
};
}
async function getAck(id: number): Promise<Ack & { rawJson: unknown }> {
if (!isConfigured) throw notConfiguredError();
const res = await fetch(joinUrl(`/api/acks/${encodeURIComponent(String(id))}`), {
headers: { Accept: "application/json" },
});
if (!res.ok) {
const detail = await readErrorBody(res);
throw new ApiError(res.status, detail || res.statusText);
}
const row = (await res.json()) as RawAckRow & { raw_json: unknown };
return { ...mapAck(row), rawJson: row.raw_json };
}
export const api = {
isConfigured,
baseUrl: BASE_URL,
health,
parse837,
parse835,
parse999,
listBatches,
getBatch,
listClaims,
@@ -538,4 +656,6 @@ export const api = {
listUnmatched,
matchRemit,
unmatchClaim,
listAcks,
getAck,
};