feat(parsers+api+ui+db): 999 ACK transaction set end-to-end (SP3 P3)
This commit is contained in:
+120
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user