feat(collection-confirm): add appendCostNote and dedupeAttachments helpers

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-02 23:48:51 +02:00
parent dca3a9a544
commit 7e1f22f363
2 changed files with 118 additions and 2 deletions

View file

@ -3,6 +3,8 @@ import { describe, expect, it } from "vitest";
import {
appendConfirmingNote,
appendNote,
appendCostNote,
dedupeAttachments,
buildConfirmNote,
buildConfirmPatch,
buildReturnPatch,
@ -363,4 +365,82 @@ describe("buildReturnPatch", () => {
"Old",
]);
});
});
describe("appendCostNote", () => {
it("writes a category=cost note with prev/new values and the actor", () => {
const result = JSON.parse(
appendCostNote("", { id: "u-1", name: "sj" }, 45, 50),
);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
actorId: "u-1",
actorName: "sj",
category: "cost",
body: { prevCost: 45, newCost: 50 },
});
expect(result[0].ts).toEqual(expect.any(String));
});
it("preserves existing notes when appending", () => {
const existing = JSON.stringify([
{
ts: "2026-07-01T00:00:00Z",
actorId: "u-1",
actorName: "sj",
category: "confirm",
body: "Confirmed for invoicing — INV-1",
},
]);
const result = JSON.parse(appendCostNote(existing, { id: "u-2", name: "ab" }, 10, 12));
expect(result).toHaveLength(2);
expect(result[0].category).toBe("confirm");
expect(result[1].category).toBe("cost");
});
it("returns the original JSON when prevCost === newCost (no-op)", () => {
const existing = JSON.stringify([
{
ts: "2026-07-01T00:00:00Z",
actorId: "u-1",
actorName: "sj",
category: "note",
body: "hello",
},
]);
expect(appendCostNote(existing, { id: "u-1", name: "sj" }, 10, 10)).toBe(existing);
});
});
describe("dedupeAttachments", () => {
it("returns one entry per distinct filename across rows", () => {
const rows = [
{ order_item_attachements: ["INV-001.pdf", "DN-7.pdf"] },
{ order_item_attachements: ["INV-002.pdf"] },
];
const out = dedupeAttachments(rows);
expect(out).toEqual([
{ filename: "INV-001.pdf", itemIds: [] },
{ filename: "DN-7.pdf", itemIds: [] },
{ filename: "INV-002.pdf", itemIds: [] },
]);
});
it("groups duplicate filenames with the list of source item ids", () => {
const rows = [
{ id: "a", order_item_attachements: ["INV-001.pdf"] },
{ id: "b", order_item_attachements: ["INV-001.pdf", "DN-7.pdf"] },
{ id: "c", order_item_attachements: ["DN-7.pdf"] },
];
const out = dedupeAttachments(rows);
expect(out).toEqual([
{ filename: "INV-001.pdf", itemIds: ["a", "b"] },
{ filename: "DN-7.pdf", itemIds: ["b", "c"] },
]);
});
it("returns an empty array when no rows have attachments", () => {
expect(dedupeAttachments([{ id: "a" }, { id: "b" }])).toEqual([]);
expect(dedupeAttachments([])).toEqual([]);
});
});

View file

@ -1,4 +1,4 @@
export type NoteCategory = "collection" | "note" | "problem" | "quantity" | "confirm";
export type NoteCategory = "collection" | "note" | "problem" | "quantity" | "confirm" | "cost";
export type NoteEntry = {
ts: string;
@ -58,7 +58,7 @@ export type PurchaseOrderGroup<T extends GroupableRow> = {
rows: T[];
};
const NOTE_CATEGORIES: NoteCategory[] = ["collection", "note", "problem", "quantity", "confirm"];
const NOTE_CATEGORIES: NoteCategory[] = ["collection", "note", "problem", "quantity", "confirm", "cost"];
function isNoteEntry(value: unknown): value is NoteEntry {
if (!value || typeof value !== "object") return false;
@ -258,3 +258,39 @@ export function buildReturnPatch(input: ReturnPatchInput): ReturnPatch {
notes,
};
}
export function appendCostNote(
rawNotes: string | null | undefined,
actor: ConfirmActor,
prevCost: number,
newCost: number,
): string {
if (prevCost === newCost) return rawNotes ?? "";
const existing = parseStoredNotes(rawNotes ?? "");
const entry = {
id: `cost-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
ts: new Date().toISOString(),
actorId: actor.id,
actorName: actor.name,
category: "cost" as const,
body: { prevCost, newCost },
};
return JSON.stringify([...existing, entry]);
}
export type DedupedAttachment = { filename: string; itemIds: string[] };
export function dedupeAttachments(
rows: Array<{ id?: string; order_item_attachements?: string[] | null }>,
): DedupedAttachment[] {
const byFilename = new Map<string, string[]>();
for (const row of rows) {
const list = row.order_item_attachements ?? [];
for (const filename of list) {
const bucket = byFilename.get(filename) ?? [];
if (row.id) bucket.push(row.id);
byFilename.set(filename, bucket);
}
}
return Array.from(byFilename.entries()).map(([filename, itemIds]) => ({ filename, itemIds }));
}