Files
cyclone/src/components/inbox/InboxRow.test.tsx
T
Nora ffacfd8665 feat(sp29): inbox rejected-row 999 ack chips + per-row Resubmit
- 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
2026-07-02 13:16:38 -06:00

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();
});
});