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:
parent
dca3a9a544
commit
7e1f22f363
2 changed files with 118 additions and 2 deletions
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 }));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue