ffacfd8665
- backend: _ack_summary_for_claims helper attaches claim_acks payload to inbox rejected-lane rows (3 tests) - frontend: InboxRow renders newest 3 AK2 chips + '+N more' overflow under the rejection reason, with '999 not linked' marker when an ack couldn't be linked to a claim - frontend: per-row Resubmit button downloads a single corrected 837 via api.serializeClaim837 (no bulk modal, no zip — one click, one .x12 file) - frontend: 2 new tests for the chip rendering and the per-row download flow
202 lines
6.3 KiB
TypeScript
202 lines
6.3 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
import { InboxRow } from "./InboxRow";
|
|
import type { InboxClaimRow, InboxCandidateRow } from "@/lib/inbox-api";
|
|
|
|
const baseClaim: InboxClaimRow = {
|
|
id: "CLP-7741",
|
|
kind: "claim",
|
|
patient_control_number: "PCN-001",
|
|
charge_amount: 1240,
|
|
payer_id: "CO-MCD",
|
|
provider_npi: "1234567890",
|
|
state: "rejected",
|
|
rejection_reason: "999 AK9 R",
|
|
rejected_at: "2026-06-20T09:12:00Z",
|
|
service_date_from: null,
|
|
score: null,
|
|
score_tier: null,
|
|
score_breakdown: null,
|
|
};
|
|
|
|
describe("InboxRow", () => {
|
|
it("renders the claim id, payer, and formatted charge", () => {
|
|
const { container } = render(
|
|
<table>
|
|
<tbody>
|
|
<InboxRow row={baseClaim} accent="oxblood" onClick={() => {}} />
|
|
</tbody>
|
|
</table>,
|
|
);
|
|
expect(container.textContent).toContain("CLP-7741");
|
|
expect(container.textContent).toContain("CO-MCD");
|
|
expect(container.textContent).toContain("$1,240.00");
|
|
});
|
|
|
|
it("renders the rejection reason when present", () => {
|
|
const { container } = render(
|
|
<table>
|
|
<tbody>
|
|
<InboxRow row={baseClaim} accent="oxblood" onClick={() => {}} />
|
|
</tbody>
|
|
</table>,
|
|
);
|
|
expect(container.textContent).toContain("999 AK9 R");
|
|
});
|
|
|
|
it("calls onClick when the row is clicked", () => {
|
|
const onClick = vi.fn();
|
|
const { container } = render(
|
|
<table>
|
|
<tbody>
|
|
<InboxRow row={baseClaim} accent="oxblood" onClick={onClick} />
|
|
</tbody>
|
|
</table>,
|
|
);
|
|
const row = container.querySelector("tr");
|
|
expect(row).not.toBeNull();
|
|
fireEvent.click(row!);
|
|
expect(onClick).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("renders the top candidate score for remit rows", () => {
|
|
const candidateRow: InboxCandidateRow = {
|
|
id: "R-1",
|
|
kind: "remit",
|
|
payer_claim_control_number: "PCN-X",
|
|
charge_amount: 500,
|
|
payer_id: "AETNA",
|
|
rendering_provider_npi: "1234567890",
|
|
service_date: null,
|
|
candidates: [
|
|
{
|
|
claim_id: "C-1",
|
|
score: 92,
|
|
tier: "strong",
|
|
breakdown: { patient: 1, date: 1, amount: 1, provider: 1 },
|
|
},
|
|
],
|
|
};
|
|
const { container } = render(
|
|
<table>
|
|
<tbody>
|
|
<InboxRow row={candidateRow} accent="amber" onClick={() => {}} />
|
|
</tbody>
|
|
</table>,
|
|
);
|
|
expect(container.textContent).toContain("92");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------
|
|
// SP29: per-AK2 chip evidence + per-row Resubmit gesture for the
|
|
// rejected-lane drill. The page renders 999 AK2 set-response codes
|
|
// inline on the row so the operator can spot "this batch had R/E/X
|
|
// rejects" without opening the drawer. The per-row Resubmit button
|
|
// downloads a single corrected 837.
|
|
// ---------------------------------------------------------------------
|
|
it("SP29: rejected row renders inline 999 ack evidence chips + Resubmit button", () => {
|
|
// 4 linked 999 AK2 set-responses — 3 will render as chips, the
|
|
// 4th triggers the "+N more" overflow indicator.
|
|
const rejectedRow: InboxClaimRow = {
|
|
...baseClaim,
|
|
id: "REJ1",
|
|
claim_acks: {
|
|
total: 4,
|
|
rejected: 2,
|
|
items: [
|
|
{
|
|
ack_id: 11,
|
|
set_control_number: "991102989",
|
|
set_accept_reject_code: "A",
|
|
ak2_index: 0,
|
|
linked_at: "2026-07-02T10:03:00Z",
|
|
},
|
|
{
|
|
ack_id: 12,
|
|
set_control_number: "991102989",
|
|
set_accept_reject_code: "R",
|
|
ak2_index: 1,
|
|
linked_at: "2026-07-02T10:02:00Z",
|
|
},
|
|
{
|
|
ack_id: 13,
|
|
set_control_number: "991102989",
|
|
set_accept_reject_code: "E",
|
|
ak2_index: 2,
|
|
linked_at: "2026-07-02T10:01:00Z",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const onResubmitOne = vi.fn();
|
|
const { container } = render(
|
|
<table>
|
|
<tbody>
|
|
<InboxRow
|
|
row={rejectedRow}
|
|
accent="oxblood"
|
|
onClick={() => {}}
|
|
onResubmitOne={onResubmitOne}
|
|
/>
|
|
</tbody>
|
|
</table>,
|
|
);
|
|
|
|
// Chip visible — the operator can read at a glance that the most
|
|
// recent AK2 from this batch was an Accept, then a Reject, then
|
|
// an Error. The "999 · A" / "999 · R" / "999 · E" text comes from
|
|
// <AckCodeChip> rendering "<stcn>·<code>".
|
|
expect(container.textContent).toContain("991102989·A");
|
|
expect(container.textContent).toContain("991102989·R");
|
|
expect(container.textContent).toContain("991102989·E");
|
|
|
|
// Overflow "+1 more" — total=4 but only 3 chips fit.
|
|
expect(container.textContent).toContain("+1 more");
|
|
|
|
// Resubmit button rendered with the per-row testid.
|
|
const btn = screen.getByTestId("resubmit-REJ1") as HTMLButtonElement;
|
|
expect(btn).toBeTruthy();
|
|
expect(btn.textContent).toBe("Resubmit");
|
|
|
|
// Click fires the per-row handler with the claim id; click does
|
|
// NOT bubble to onClick (the row-onClick is for drilldown, the
|
|
// button is a separate download gesture).
|
|
fireEvent.click(btn);
|
|
expect(onResubmitOne).toHaveBeenCalledTimes(1);
|
|
expect(onResubmitOne).toHaveBeenCalledWith("REJ1");
|
|
});
|
|
|
|
it("SP29: rejected row with no linked 999 acks shows the '999 not linked' orphan marker", () => {
|
|
// A rejected claim whose 999 ack didn't match any ST02 in the
|
|
// batch — the operator needs to know the drill lacked ack
|
|
// evidence. The "999 not linked" italic line is the marker.
|
|
const rejectedRow: InboxClaimRow = {
|
|
...baseClaim,
|
|
id: "REJ-ORPH",
|
|
claim_acks: {
|
|
total: 0,
|
|
rejected: 0,
|
|
items: [],
|
|
},
|
|
};
|
|
const onResubmitOne = vi.fn();
|
|
const { container } = render(
|
|
<table>
|
|
<tbody>
|
|
<InboxRow
|
|
row={rejectedRow}
|
|
accent="oxblood"
|
|
onClick={() => {}}
|
|
onResubmitOne={onResubmitOne}
|
|
/>
|
|
</tbody>
|
|
</table>,
|
|
);
|
|
expect(container.textContent).toContain("999 not linked");
|
|
// The Resubmit button still renders so the operator can fix the
|
|
// claim without first manually linking the orphan.
|
|
expect(screen.getByTestId("resubmit-REJ-ORPH")).toBeTruthy();
|
|
});
|
|
});
|