feat(sp25): extend useMergedTail to handle acks + ta1_acks slices
The Acks page needs two merge hooks (one per ack flavor) and both
shapes use numeric database ids rather than the string ids that
Claim/Remittance/Activity use. The generic constraint widens to
`T extends { id: string | number }` and the dedup Set normalizes
to String(id) so a base item with id=2 and a tail item with id=2
collide correctly.
Adds two switch cases that mirror the claims/remittances keyed-by-id
pattern: iterate the order array, look up the matching value in the
dict, drop undefined holes defensively.
Two tests assert the new slices order by their order arrays and dedup
against base items.
This commit is contained in:
@@ -7,7 +7,7 @@ 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 { Claim, Remittance, Activity } from "@/types";
|
||||
import type { Ack, Claim, Remittance, Activity, Ta1Ack } from "@/types";
|
||||
|
||||
/**
|
||||
* Same `renderHook` shim used in `useClaimDetail`, `useDrawerUrlState`,
|
||||
@@ -88,6 +88,34 @@ function activity(id: string, message: string): Activity {
|
||||
};
|
||||
}
|
||||
|
||||
function ack(id: number, sourceBatchId: string): Ack {
|
||||
return {
|
||||
id,
|
||||
sourceBatchId,
|
||||
acceptedCount: 1,
|
||||
rejectedCount: 0,
|
||||
receivedCount: 1,
|
||||
ackCode: "A",
|
||||
parsedAt: "2026-07-02T12:00:00Z",
|
||||
patientControlNumber: `PCN-${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
function ta1Ack(id: number, controlNumber: string): Ta1Ack {
|
||||
return {
|
||||
id,
|
||||
controlNumber,
|
||||
ackCode: "A",
|
||||
noteCode: "000",
|
||||
interchangeDate: "2026-07-02",
|
||||
interchangeTime: "1200",
|
||||
senderId: "S",
|
||||
receiverId: "R",
|
||||
sourceBatchId: `TA1-${id}`,
|
||||
parsedAt: "2026-07-02T12:00:00Z",
|
||||
};
|
||||
}
|
||||
|
||||
describe("useMergedTail", () => {
|
||||
beforeEach(() => {
|
||||
// Singleton store — clear each slice between tests so cases are
|
||||
@@ -95,6 +123,8 @@ describe("useMergedTail", () => {
|
||||
useTailStore.getState().reset("claims");
|
||||
useTailStore.getState().reset("remittances");
|
||||
useTailStore.getState().reset("activity");
|
||||
useTailStore.getState().reset("acks");
|
||||
useTailStore.getState().reset("ta1_acks");
|
||||
});
|
||||
|
||||
it("test_merges_base_and_tail_by_id_base_first", () => {
|
||||
@@ -228,4 +258,50 @@ describe("useMergedTail", () => {
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SP25: the Acks page mounts two merge hooks — one per ack flavor.
|
||||
// Both have numeric ids (database row ids) instead of string ids, so
|
||||
// the dedup must compare via String(id) to keep the keyed-by-id
|
||||
// contract intact.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it("test_acks_resource_orders_by_ack_order_and_dedupes", () => {
|
||||
const base = [ack(1, "BASE-A"), ack(2, "BASE-B")];
|
||||
const { addAck } = useTailStore.getState();
|
||||
addAck(ack(2, "REPLAY-B")); // duplicate id — must be dropped
|
||||
addAck(ack(3, "TAIL-C"));
|
||||
addAck(ack(4, "TAIL-D"));
|
||||
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useMergedTail("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]?.sourceBatchId).toBe("BASE-B");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("test_ta1_acks_resource_orders_by_ta1_ack_order_and_dedupes", () => {
|
||||
const base = [ta1Ack(1, "000000001")];
|
||||
const { addTa1Ack } = useTailStore.getState();
|
||||
addTa1Ack(ta1Ack(2, "000000002"));
|
||||
addTa1Ack(ta1Ack(3, "000000003"));
|
||||
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useMergedTail("ta1_acks", base),
|
||||
);
|
||||
|
||||
const merged = result.current ?? [];
|
||||
expect(merged.map((a) => a.id)).toEqual([1, 2, 3]);
|
||||
expect(merged[0]?.controlNumber).toBe("000000001");
|
||||
expect(merged[1]?.controlNumber).toBe("000000002");
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
// every render of the caller, so memo deps would invalidate anyway.
|
||||
// The merge is cheap (one Set build + two filters) and zustand's
|
||||
// selector ensures we only re-render when the slice actually changes.
|
||||
// - SP25: the `id` type is widened to `string | number` because `Ack`
|
||||
// and `Ta1Ack` use numeric database ids while `Claim` / `Remittance`
|
||||
// use string ids. Dedup normalizes via `String(...)`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { useTailStore } from "@/store/tail-store";
|
||||
import type { TailResource } from "@/lib/tail-stream";
|
||||
|
||||
export function useMergedTail<T extends { id: string }>(
|
||||
export function useMergedTail<T extends { id: string | number }>(
|
||||
resource: TailResource,
|
||||
baseItems: T[],
|
||||
filterFn?: (item: T) => boolean,
|
||||
@@ -56,15 +59,39 @@ export function useMergedTail<T extends { id: string }>(
|
||||
// Activity is already an array — the store appends in arrival
|
||||
// order, so iteration order is exactly what we want.
|
||||
return s.activity as unknown[];
|
||||
// SP25: same keyed-by-id shape as claims/remittances, just with
|
||||
// numeric ids. The store keys the dict by String(id); the order
|
||||
// array carries the numeric id for arrival-order iteration.
|
||||
case "acks": {
|
||||
const out: unknown[] = [];
|
||||
for (const id of s.ackOrder) {
|
||||
const v = s.acks[String(id)];
|
||||
if (v !== undefined) out.push(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
case "ta1_acks": {
|
||||
const out: unknown[] = [];
|
||||
for (const id of s.ta1AckOrder) {
|
||||
const v = s.ta1Acks[String(id)];
|
||||
if (v !== undefined) out.push(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Normalize ids to strings so numeric (Ack/Ta1Ack) and string
|
||||
// (Claim/Remittance/Activity) ids dedup correctly against each other.
|
||||
// (The hook is generic over `T extends { id: string | number }`, but
|
||||
// the store's dedup set is a Set<string> internally — using the
|
||||
// string form here keeps the comparison symmetric.)
|
||||
const baseIds = new Set<string>();
|
||||
for (const b of baseItems) baseIds.add(b.id);
|
||||
for (const b of baseItems) baseIds.add(String(b.id));
|
||||
|
||||
const tailAfterDedup: unknown[] = [];
|
||||
for (const t of tailSlice) {
|
||||
const id = (t as { id: string }).id;
|
||||
const id = String((t as { id: string | number }).id);
|
||||
if (baseIds.has(id)) continue;
|
||||
tailAfterDedup.push(t);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user