feat(sp28): claimAcks slice + useTailStream/useMergedTail dispatch

This commit is contained in:
Nora
2026-07-02 11:31:17 -06:00
parent a97f1d1350
commit c54b2c1867
8 changed files with 326 additions and 6 deletions
+83 -1
View File
@@ -7,7 +7,14 @@ import { createRoot, type Root } from "react-dom/client";
import { beforeEach, describe, expect, it } from "vitest";
import { useMergedTail } from "./useMergedTail";
import { useTailStore } from "@/store/tail-store";
import type { Ack, Claim, Remittance, Activity, Ta1Ack } from "@/types";
import type {
Ack,
Claim,
ClaimAck,
Remittance,
Activity,
Ta1Ack,
} from "@/types";
/**
* Same `renderHook` shim used in `useClaimDetail`, `useDrawerUrlState`,
@@ -116,6 +123,22 @@ function ta1Ack(id: number, controlNumber: string): Ta1Ack {
};
}
function claimAck(id: number, claimId: string): ClaimAck {
return {
id,
claimId,
batchId: null,
ackId: 99,
ackKind: "999",
ak2Index: 0,
setControlNumber: "991102989",
setAcceptRejectCode: "A",
linkedAt: "2026-07-02T12:00:00Z",
linkedBy: "auto",
claimState: "received",
};
}
describe("useMergedTail", () => {
beforeEach(() => {
// Singleton store — clear each slice between tests so cases are
@@ -125,6 +148,7 @@ describe("useMergedTail", () => {
useTailStore.getState().reset("activity");
useTailStore.getState().reset("acks");
useTailStore.getState().reset("ta1_acks");
useTailStore.getState().reset("claim-acks");
});
it("test_merges_base_and_tail_by_id_base_first", () => {
@@ -304,4 +328,62 @@ describe("useMergedTail", () => {
unmount();
});
// -------------------------------------------------------------------------
// SP28: the claim↔ack link slice. Same keyed-by-id shape as the SP25
// acks/ta1_acks branches — numeric id, ordered arrival via
// `claimAckOrder`, deduped against baseItems by `String(id)`. The
// ClaimDrawer and AckDrawer both mount this slice and apply a
// page-specific filter (e.g. `link.claimId === claimId`) so the merge
// shape must remain symmetric with the existing branches.
// -------------------------------------------------------------------------
it("test_claim_acks_resource_orders_by_claim_ack_order_and_dedupes", () => {
const base = [claimAck(1, "CLM-A"), claimAck(2, "CLM-A")];
const { addClaimAck } = useTailStore.getState();
addClaimAck(claimAck(2, "CLM-A-replay")); // duplicate id — must be dropped
addClaimAck(claimAck(3, "CLM-B"));
addClaimAck(claimAck(4, "CLM-B"));
const { result, unmount } = renderHook(() =>
useMergedTail("claim-acks", base),
);
const merged = result.current ?? [];
// Base first, then tail in arrival order. Tail's "2" is dropped
// because the base already has id 2.
expect(merged.map((a) => a.id)).toEqual([1, 2, 3, 4]);
// Base version wins for id 2.
expect(merged[1]?.claimId).toBe("CLM-A");
unmount();
});
it("test_claim_acks_filter_predicate_drops_tail_items_that_dont_match", () => {
// The ClaimDrawer panel mounts a filter `link.claimId === claimId`
// so only this claim's acks show up. Verify the merge keeps the
// filter AFTER dedup against baseItems (mirrors the SP25 claim
// branch shape).
const base = [claimAck(1, "CLM-A")];
const { addClaimAck } = useTailStore.getState();
addClaimAck(claimAck(2, "CLM-A"));
addClaimAck(claimAck(3, "CLM-B"));
addClaimAck(claimAck(4, "CLM-A"));
const { result, unmount } = renderHook(() =>
useMergedTail(
"claim-acks",
base,
(link) => link.claimId === "CLM-A",
),
);
const merged = result.current ?? [];
// Filter only applies to tail (base is kept verbatim by contract);
// tail's id 3 (CLM-B) is filtered out, leaving 2 and 4 in
// arrival order after the base 1.
expect(merged.map((a) => a.id)).toEqual([1, 2, 4]);
unmount();
});
});