687 lines
21 KiB
TypeScript
687 lines
21 KiB
TypeScript
/**
|
|
* Cyclone API client.
|
|
*
|
|
* Talks to the FastAPI backend that lives at `VITE_API_BASE_URL`
|
|
* (defaulting to an empty string → "no backend configured" mode). When the
|
|
* base URL is empty, every method throws `notConfiguredError()`; the
|
|
* higher-level hooks in `src/hooks/` catch that by reading from the
|
|
* in-memory zustand store instead. This keeps the pages agnostic of the
|
|
* switch.
|
|
*
|
|
* Endpoints:
|
|
* - `parse837(file, { onProgress })` POSTs to `/api/parse-837`. With
|
|
* `onProgress` set, streams NDJSON (one `{type,data}` per line) so the
|
|
* UI can render claims incrementally. Without it, asks the backend for
|
|
* a single JSON object via `Accept: application/json`.
|
|
* - `parse835(...)` mirrors the 837 shape for `/api/parse-835`.
|
|
* - `health()` GETs `/api/health` and returns `{ status, version }`.
|
|
* - `listBatches / getBatch / listClaims / listRemittances / listProviders
|
|
* / 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 {
|
|
Ack,
|
|
ClaimDetail,
|
|
ClaimOutput,
|
|
ClaimPayment,
|
|
Envelope,
|
|
FinancialInfo,
|
|
MatchResponse,
|
|
NdjsonEvent,
|
|
ParseResult837,
|
|
ParseResult835,
|
|
Payer,
|
|
Payer835,
|
|
Payee835,
|
|
ReassociationTrace,
|
|
UnmatchedClaim,
|
|
UnmatchedResponse,
|
|
BatchSummary as ParserBatchSummary,
|
|
} from "@/types";
|
|
|
|
const BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? "";
|
|
|
|
export const isConfigured = BASE_URL.length > 0;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type NdjsonProgressEvent =
|
|
| { type: "envelope"; data: Envelope | null }
|
|
| { type: "claim"; data: ClaimOutput }
|
|
| { type: "claim_payment"; data: ClaimPayment }
|
|
| { type: "financial_info"; data: FinancialInfo }
|
|
| { type: "trace"; data: ReassociationTrace }
|
|
| { type: "payer"; data: Payer | Payer835 }
|
|
| { type: "payee"; data: Payee835 }
|
|
| { type: "summary"; data: ParserBatchSummary };
|
|
|
|
export type ParseProgress = (event: NdjsonProgressEvent) => void;
|
|
|
|
export interface ParseOptions {
|
|
/** Defaults to "co_medicaid" for 837 / "co_medicaid_835" for 835. */
|
|
payer?: string;
|
|
/** Promote validation warnings to errors on the backend. */
|
|
strict?: boolean;
|
|
/** Include raw EDI segments in the response (default: true). */
|
|
includeRawSegments?: boolean;
|
|
/**
|
|
* Optional progress callback. When provided, the client streams NDJSON from
|
|
* the backend and invokes the callback for every line. When omitted, the
|
|
* client requests a single JSON object via `Accept: application/json`.
|
|
*/
|
|
onProgress?: ParseProgress;
|
|
}
|
|
|
|
export interface HealthResponse {
|
|
status: string;
|
|
version: string;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// List-endpoint types (GET surfaces, sub-project 1)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface ListClaimsParams {
|
|
batch_id?: string;
|
|
status?: string;
|
|
payer?: string;
|
|
provider_npi?: string;
|
|
date_from?: string;
|
|
date_to?: string;
|
|
sort?: string;
|
|
order?: "asc" | "desc";
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
export interface ListRemittancesParams {
|
|
batch_id?: string;
|
|
payer?: string;
|
|
claim_id?: string;
|
|
date_from?: string;
|
|
date_to?: string;
|
|
sort?: string;
|
|
order?: "asc" | "desc";
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
export interface ListProvidersParams {
|
|
npi?: string;
|
|
state?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
export interface ListActivityParams {
|
|
kind?: string;
|
|
since?: string;
|
|
limit?: number;
|
|
}
|
|
|
|
export interface PaginatedResponse<T> {
|
|
items: T[];
|
|
total: number;
|
|
returned: number;
|
|
has_more: boolean;
|
|
}
|
|
|
|
/**
|
|
* Lightweight summary used by the GET /api/batches list endpoint. Distinct
|
|
* from the parser's `BatchSummary` (which lives in `@/types` and carries the
|
|
* full validation + per-rule counts) — this one is what the UI needs to
|
|
* render a list row.
|
|
*/
|
|
export interface BatchSummary {
|
|
id: string;
|
|
kind: "837p" | "835";
|
|
inputFilename: string;
|
|
parsedAt: string;
|
|
claimCount: number;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Low-level helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function notConfiguredError(): Error {
|
|
return new Error(
|
|
"API not configured. Set VITE_API_BASE_URL in .env.local to use the backend."
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
return `${BASE_URL.replace(/\/$/, "")}${path}`;
|
|
}
|
|
|
|
async function readErrorBody(res: Response): Promise<string> {
|
|
try {
|
|
const t = await res.text();
|
|
if (!t) return "";
|
|
// FastAPI errors are `{ "error": "...", "detail": "..." }`. Surface the
|
|
// detail if present, else the raw text.
|
|
try {
|
|
const obj = JSON.parse(t) as { detail?: unknown; error?: unknown };
|
|
if (typeof obj.detail === "string") return obj.detail;
|
|
if (typeof obj.error === "string") return obj.error;
|
|
} catch {
|
|
// not JSON; fall through
|
|
}
|
|
return t;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/** Build a query string, skipping undefined/empty params. */
|
|
function qs(params: Record<string, unknown> | undefined): string {
|
|
if (!params) return "";
|
|
const u = new URLSearchParams();
|
|
for (const [k, v] of Object.entries(params)) {
|
|
if (v === undefined || v === null || v === "") continue;
|
|
u.set(k, String(v));
|
|
}
|
|
const s = u.toString();
|
|
return s ? `?${s}` : "";
|
|
}
|
|
|
|
async function parseNdjsonStream(
|
|
res: Response,
|
|
onProgress: ParseProgress
|
|
): Promise<ParserBatchSummary> {
|
|
if (!res.body) {
|
|
throw new Error("Response had no body to stream.");
|
|
}
|
|
const reader = res.body.getReader();
|
|
const decoder = new TextDecoder("utf-8");
|
|
let buffer = "";
|
|
let lastSummary: ParserBatchSummary | null = null;
|
|
|
|
// Read the body chunk-by-chunk. A `while (true)` loop is the standard
|
|
// pattern for ReadableStreamDefaultReader; `done` breaks us out.
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
// Split on newlines. The backend terminates every line with "\n".
|
|
let nl = buffer.indexOf("\n");
|
|
while (nl !== -1) {
|
|
const line = buffer.slice(0, nl).replace(/\r$/, "");
|
|
buffer = buffer.slice(nl + 1);
|
|
if (line.length > 0) {
|
|
let event: NdjsonEvent;
|
|
try {
|
|
event = JSON.parse(line) as NdjsonEvent;
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Failed to parse NDJSON line: ${
|
|
err instanceof Error ? err.message : String(err)
|
|
}\n${line}`
|
|
);
|
|
}
|
|
onProgress(event as NdjsonProgressEvent);
|
|
if (event.type === "summary") {
|
|
lastSummary = event.data as ParserBatchSummary;
|
|
}
|
|
}
|
|
nl = buffer.indexOf("\n");
|
|
}
|
|
}
|
|
|
|
// Flush any trailing partial line (no final newline).
|
|
const tail = buffer.replace(/\r$/, "");
|
|
if (tail.length > 0) {
|
|
const event = JSON.parse(tail) as NdjsonEvent;
|
|
onProgress(event as NdjsonProgressEvent);
|
|
if (event.type === "summary") {
|
|
lastSummary = event.data as ParserBatchSummary;
|
|
}
|
|
}
|
|
|
|
if (!lastSummary) {
|
|
throw new Error("Stream ended without a summary line.");
|
|
}
|
|
return lastSummary;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public surface — parser endpoints
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function parse837(
|
|
file: File,
|
|
options: ParseOptions = {}
|
|
): Promise<ParseResult837 | ParserBatchSummary> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const { payer = "co_medicaid", strict = false, includeRawSegments = true, onProgress } = options;
|
|
|
|
const params = new URLSearchParams({
|
|
payer,
|
|
strict: String(strict),
|
|
include_raw_segments: String(includeRawSegments),
|
|
});
|
|
|
|
const form = new FormData();
|
|
form.append("file", file, file.name);
|
|
|
|
const accept = onProgress ? "application/x-ndjson" : "application/json";
|
|
const res = await fetch(joinUrl(`/api/parse-837?${params.toString()}`), {
|
|
method: "POST",
|
|
body: form,
|
|
headers: { Accept: accept },
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const detail = await readErrorBody(res);
|
|
throw new Error(
|
|
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
|
);
|
|
}
|
|
|
|
if (onProgress) {
|
|
return parseNdjsonStream(res, onProgress);
|
|
}
|
|
return (await res.json()) as ParseResult837;
|
|
}
|
|
|
|
async function parse835(
|
|
file: File,
|
|
options: ParseOptions = {}
|
|
): Promise<ParseResult835 | ParserBatchSummary> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const { payer = "co_medicaid_835", strict = false, includeRawSegments = true, onProgress } = options;
|
|
|
|
const params = new URLSearchParams({
|
|
payer,
|
|
strict: String(strict),
|
|
include_raw_segments: String(includeRawSegments),
|
|
});
|
|
|
|
const form = new FormData();
|
|
form.append("file", file, file.name);
|
|
|
|
const accept = onProgress ? "application/x-ndjson" : "application/json";
|
|
const res = await fetch(joinUrl(`/api/parse-835?${params.toString()}`), {
|
|
method: "POST",
|
|
body: form,
|
|
headers: { Accept: accept },
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const detail = await readErrorBody(res);
|
|
throw new Error(
|
|
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
|
);
|
|
}
|
|
|
|
if (onProgress) {
|
|
return parseNdjsonStream(res, onProgress);
|
|
}
|
|
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"));
|
|
if (!res.ok) {
|
|
const detail = await readErrorBody(res);
|
|
throw new Error(
|
|
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
|
);
|
|
}
|
|
return (await res.json()) as HealthResponse;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public surface — GET endpoints (sub-project 1)
|
|
// All throw `notConfiguredError` when no backend is wired. The hooks in
|
|
// `src/hooks/` are responsible for the in-memory zustand fallback.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function listBatches(limit?: number): Promise<BatchSummary[]> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const params: Record<string, unknown> = {};
|
|
if (limit !== undefined) params.limit = limit;
|
|
const res = await fetch(joinUrl(`/api/batches${qs(params)}`), {
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
if (!res.ok) {
|
|
const detail = await readErrorBody(res);
|
|
throw new Error(
|
|
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
|
);
|
|
}
|
|
const body = (await res.json()) as { items: BatchSummary[] };
|
|
return body.items;
|
|
}
|
|
|
|
async function getBatch(id: string): Promise<ParseResult837 | ParseResult835> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const res = await fetch(joinUrl(`/api/batches/${encodeURIComponent(id)}`), {
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
if (!res.ok) {
|
|
const detail = await readErrorBody(res);
|
|
throw new Error(
|
|
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
|
);
|
|
}
|
|
return (await res.json()) as ParseResult837 | ParseResult835;
|
|
}
|
|
|
|
async function listClaims<T = unknown>(
|
|
params: ListClaimsParams
|
|
): Promise<PaginatedResponse<T>> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const res = await fetch(
|
|
joinUrl(`/api/claims${qs(params as Record<string, unknown>)}`),
|
|
{ headers: { Accept: "application/json" } }
|
|
);
|
|
if (!res.ok) {
|
|
const detail = await readErrorBody(res);
|
|
throw new Error(
|
|
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
|
);
|
|
}
|
|
return (await res.json()) as PaginatedResponse<T>;
|
|
}
|
|
|
|
/**
|
|
* Fetch one claim with the full SP4 detail-drawer context (header, state,
|
|
* parties, validation, service lines, diagnoses, raw segments, recent
|
|
* ``stateHistory``, and a populated ``matchedRemittance`` when paired).
|
|
*
|
|
* Drives ``GET /api/claims/{claim_id}``. Throws ``ApiError`` on non-2xx —
|
|
* including 404, which the spec §3.4 calls out as a distinct "claim
|
|
* doesn't exist" state in the drawer (separate from a transient fetch
|
|
* failure). Callers should branch on ``error.status === 404`` to render
|
|
* the not-found state instead of the generic error toast.
|
|
*/
|
|
async function getClaimDetail(id: string): Promise<ClaimDetail> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const res = await fetch(joinUrl(`/api/claims/${encodeURIComponent(id)}`), {
|
|
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 ClaimDetail;
|
|
}
|
|
|
|
async function listRemittances<T = unknown>(
|
|
params: ListRemittancesParams
|
|
): Promise<PaginatedResponse<T>> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const res = await fetch(
|
|
joinUrl(`/api/remittances${qs(params as Record<string, unknown>)}`),
|
|
{ headers: { Accept: "application/json" } }
|
|
);
|
|
if (!res.ok) {
|
|
const detail = await readErrorBody(res);
|
|
throw new Error(
|
|
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
|
);
|
|
}
|
|
return (await res.json()) as PaginatedResponse<T>;
|
|
}
|
|
|
|
/**
|
|
* Fetch one remittance with its labeled CAS `adjustments` array.
|
|
* Throws `ApiError` on 404 so callers can branch on `.status`.
|
|
*/
|
|
async function getRemittance<T = unknown>(id: string): Promise<T> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const res = await fetch(joinUrl(`/api/remittances/${encodeURIComponent(id)}`), {
|
|
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 T;
|
|
}
|
|
|
|
async function listProviders<T = unknown>(
|
|
params: ListProvidersParams = {}
|
|
): Promise<PaginatedResponse<T>> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const res = await fetch(
|
|
joinUrl(`/api/providers${qs(params as Record<string, unknown>)}`),
|
|
{ headers: { Accept: "application/json" } }
|
|
);
|
|
if (!res.ok) {
|
|
const detail = await readErrorBody(res);
|
|
throw new Error(
|
|
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
|
);
|
|
}
|
|
return (await res.json()) as PaginatedResponse<T>;
|
|
}
|
|
|
|
async function listActivity<T = unknown>(
|
|
params: ListActivityParams = {}
|
|
): Promise<PaginatedResponse<T>> {
|
|
if (!isConfigured) throw notConfiguredError();
|
|
const res = await fetch(
|
|
joinUrl(`/api/activity${qs(params as Record<string, unknown>)}`),
|
|
{ headers: { Accept: "application/json" } }
|
|
);
|
|
if (!res.ok) {
|
|
const detail = await readErrorBody(res);
|
|
throw new Error(
|
|
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
|
);
|
|
}
|
|
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 };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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,
|
|
getClaimDetail,
|
|
listRemittances,
|
|
getRemittance,
|
|
listProviders,
|
|
listActivity,
|
|
listUnmatched,
|
|
matchRemit,
|
|
unmatchClaim,
|
|
listAcks,
|
|
getAck,
|
|
};
|