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>
125 lines
4.7 KiB
TypeScript
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" });
|
|
});
|
|
});
|