WFDASH-WEBAPP/src/lib/collection-ai-card.test.ts
Dave afdfbce71d fix(pos): never sell or list an item at R0; customer lines go to the price list
Owner's rule: the POS counter only carries priced stock.

- POS catalogue lists only active stock items with selling_price > 0
  (POS_CATALOG_FILTER); checkout refuses any line priced at R0 or less.
- New stock order now gives a new stock item the PO line's cost and the
  default 80% markup. It used to drop the cost, which is how 157 stock
  items reached the till at R0.
- Collection Confirm "Add to POS" becomes "Add to price list": customer
  lines go to price_list at cost + 80% (needs SKU, cost and supplier),
  never into stock_items. Route renamed to add-to-price-list.
- scripts/2026-10-30-pos-zero-price-cleanup.ts: priced 79 items from their
  PO line cost, switched off 48 with no cost and no stock, left 30 stocked
  ones active for costing (already applied on live PB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:56:03 +02:00

125 lines
4.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { makeAiRow, makeCheck } from "@/components/collection-confirm/ai-fixtures";
import { aiInvoiceDefaults, cardDocument, cardFindings, cardNotes, priceListResultToast } from "./collection-ai-card";
const doc = (read_id: string, findings: string[], doc_date = "2026-10-16") => ({
read_id,
doc_type: "tax_invoice" as const,
document_no: read_id,
doc_date,
supplier_name: "S",
findings,
lines: [],
});
describe("collection-ai-card", () => {
it("unions the AI's reading notes across distinct reads, deduped in order", () => {
const rows = [
makeAiRow({ id: "1", ai_doc: { ...doc("r1", []), notes: ["Delivery note, not a tax invoice", "Rotated"] } }),
makeAiRow({ id: "2", ai_doc: { ...doc("r1", []), notes: ["Delivery note, not a tax invoice", "Rotated"] } }),
makeAiRow({ id: "3", ai_doc: { ...doc("r2", []), notes: ["Rotated", "Year not printed"] } }),
makeAiRow({ id: "4", ai_doc: doc("r3", []) }),
];
expect(cardNotes(rows)).toEqual(["Delivery note, not a tax invoice", "Rotated", "Year not printed"]);
});
it("unions findings across distinct reads, deduped in order", () => {
const rows = [
makeAiRow({ id: "1", ai_doc: doc("r1", ["a", "b"]) }),
makeAiRow({ id: "2", ai_doc: doc("r1", ["a", "b"]) }),
makeAiRow({ id: "3", ai_doc: doc("r2", ["b", "c"]) }),
makeAiRow({ id: "4", ai_doc: null }),
];
expect(cardFindings(rows)).toEqual(["a", "b", "c"]);
});
it("adds each row's line findings after the document findings, deduped", () => {
const rows = [
makeAiRow({
id: "1",
ai_doc: doc("r1", ["a"]),
ai_check: makeCheck({ findings: ["Not found on r1.", "a"] }),
}),
makeAiRow({
id: "2",
ai_doc: doc("r1", ["a"]),
ai_check: makeCheck({ findings: ["Not found on r1.", "AI couldn't pair this document: boom"] }),
}),
makeAiRow({ id: "3", ai_doc: doc("r2", ["b"]) }),
];
expect(cardFindings(rows)).toEqual([
"a",
"b",
"Not found on r1.",
"AI couldn't pair this document: boom",
]);
});
it("summarises Add to price list results for the toast", () => {
const r = (status: string) => ({ status });
expect(priceListResultToast([r("added"), r("added")])).toEqual({
level: "success",
message: "Added 2 items to the price list",
});
expect(priceListResultToast([r("added"), r("already_in_pos")])).toEqual({
level: "success",
message: "Added 1 item to the price list",
});
expect(
priceListResultToast([r("added"), r("failed"), r("failed"), r("needs_sku"), r("needs_cost")]),
).toEqual({
level: "warning",
message: "1 added, 2 failed, 1 needs a SKU, 1 needs a cost",
});
expect(priceListResultToast([r("needs_cost"), r("needs_cost"), r("needs_supplier")])).toEqual({
level: "warning",
message: "0 added, 2 need a cost, 1 needs a supplier",
});
});
it("picks the newest document for the chip", () => {
const rows = [
makeAiRow({ id: "1", ai_doc: doc("r1", [], "2026-10-10") }),
makeAiRow({ id: "2", ai_doc: doc("r2", [], "2026-10-16") }),
];
expect(cardDocument(rows)?.read_id).toBe("r2");
expect(cardDocument([makeAiRow({ ai_doc: null })])).toBeNull();
});
it("derives invoice defaults from the card's (newest) tax invoice", () => {
const rows = [
makeAiRow({ id: "1", ai_check: null, ai_doc: null }),
makeAiRow({ id: "2", ai_doc: doc("5028256", [], "2026-10-16") }),
];
expect(aiInvoiceDefaults(rows)).toEqual({ number: "5028256", date: "2026-10-16" });
});
it("uses the same newest document as the chip", () => {
const rows = [
makeAiRow({ id: "1", ai_doc: doc("OLD-1", [], "2026-10-10") }),
makeAiRow({ id: "2", ai_doc: doc("NEW-2", [], "2026-10-16") }),
];
expect(aiInvoiceDefaults(rows)).toEqual({ number: "NEW-2", date: "2026-10-16" });
});
it("never pre-fills from a delivery note", () => {
const dn = { ...doc("DN-7", [], "2026-10-16"), doc_type: "delivery_note" as const };
const rows = [
makeAiRow({ id: "1", ai_doc: doc("INV-1", [], "2026-10-10") }),
makeAiRow({ id: "2", ai_doc: dn }),
];
// The chip shows the newer delivery note, so nothing is pre-filled.
expect(aiInvoiceDefaults(rows)).toBeUndefined();
});
it("returns undefined without any document number", () => {
expect(aiInvoiceDefaults([makeAiRow({ ai_check: null, ai_doc: null })])).toBeUndefined();
expect(aiInvoiceDefaults([makeAiRow({ ai_doc: doc("", []) })])).toBeUndefined();
});
it("omits an invalid date", () => {
const rows = [makeAiRow({ ai_doc: doc("5028256", [], "") })];
expect(aiInvoiceDefaults(rows)).toEqual({ number: "5028256" });
});
});