New /delivery-pool page, a clone of the Collection Pool for deliveries with no pending stage. Invoiced lines are grouped per company, one card per customer invoice; a card expands to its lines. Assign a driver and day per invoice (or bulk per company); uploading the signed copy adds it to every line, marks the lines completed + delivered_at, and closes a delivery note once none of its lines is still open. - Driver lives in new order_items.delivery_driver / delivery_assigned_at (npm run pb:migrate:delivery-pool), never drv_id (collection driver). - Pool starts at DELIVERY_POOL_START (2026-10-30); lines whose DN is already completed/cancelled (POS counter sales) are left out. - Service + /api/v1/delivery-pool routes (browser session); the service 503s until the migration has run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
5.3 KiB
TypeScript
122 lines
5.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
deliveryAssignmentState,
|
|
filterDeliveryPool,
|
|
groupDeliveryPool,
|
|
normalizeInvoiceNumber,
|
|
type DeliveryPoolLine,
|
|
} from "./delivery-pool";
|
|
|
|
function line(over: Partial<DeliveryPoolLine>): DeliveryPoolLine {
|
|
return {
|
|
id: "l1",
|
|
description: "M20 nut",
|
|
quantity: 1,
|
|
selling_price: 10,
|
|
customer_invoice_number: "INV100",
|
|
delivery_note_number: "",
|
|
invoiced_date: "2026-10-30",
|
|
delivery_driver: "",
|
|
delivery_assigned_at: "",
|
|
order_id: "o1",
|
|
order_number: "ORD-1",
|
|
company_id: "c1",
|
|
company_name: "Acme",
|
|
invoice_pdf: "",
|
|
...over,
|
|
};
|
|
}
|
|
|
|
describe("normalizeInvoiceNumber", () => {
|
|
it("treats bare digits and the INV prefix as the same invoice", () => {
|
|
expect(normalizeInvoiceNumber("30352")).toBe("INV30352");
|
|
expect(normalizeInvoiceNumber("inv 30352")).toBe("INV30352");
|
|
expect(normalizeInvoiceNumber("INV-30352")).toBe("INV30352");
|
|
});
|
|
it("keeps non-numeric numbers and blanks", () => {
|
|
expect(normalizeInvoiceNumber("ABC-9")).toBe("ABC-9");
|
|
expect(normalizeInvoiceNumber(" ")).toBe("");
|
|
expect(normalizeInvoiceNumber(null)).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("groupDeliveryPool", () => {
|
|
it("groups per company, then one card per invoice", () => {
|
|
const groups = groupDeliveryPool([
|
|
line({ id: "a", customer_invoice_number: "30352", quantity: 2, selling_price: 5 }),
|
|
line({ id: "b", customer_invoice_number: "INV30352", delivery_note_number: "DN1" }),
|
|
line({ id: "c", customer_invoice_number: "INV1", company_id: "c2", company_name: "Bolt Co" }),
|
|
]);
|
|
expect(groups.map((g) => g.name)).toEqual(["Acme", "Bolt Co"]);
|
|
expect(groups[0].invoices).toHaveLength(1);
|
|
const inv = groups[0].invoices[0];
|
|
expect(inv.invoiceNumber).toBe("INV30352");
|
|
expect(inv.lines.map((l) => l.id)).toEqual(["a", "b"]);
|
|
expect(inv.total).toBe(20);
|
|
expect(inv.dnNumbers).toEqual(["DN1"]);
|
|
expect(groups[0].total).toBe(20);
|
|
});
|
|
|
|
it("skips lines without an invoice number and puts unknown companies last", () => {
|
|
const groups = groupDeliveryPool([
|
|
line({ id: "x", customer_invoice_number: "" }),
|
|
line({ id: "u", customer_invoice_number: "INV9", company_id: "", company_name: "" }),
|
|
line({ id: "z", customer_invoice_number: "INV8", company_id: "c9", company_name: "Zulu" }),
|
|
]);
|
|
expect(groups.map((g) => g.name)).toEqual(["Zulu", "Unknown company"]);
|
|
});
|
|
|
|
it("reports a shared assignment, or mixed when the lines disagree", () => {
|
|
const same = groupDeliveryPool([
|
|
line({ id: "a", delivery_driver: "d1", delivery_assigned_at: "2026-10-31" }),
|
|
line({ id: "b", delivery_driver: "d1", delivery_assigned_at: "2026-10-31" }),
|
|
])[0].invoices[0];
|
|
expect(same).toMatchObject({ driverId: "d1", assignedAt: "2026-10-31", mixedAssignment: false });
|
|
|
|
const mixed = groupDeliveryPool([
|
|
line({ id: "a", delivery_driver: "d1", delivery_assigned_at: "2026-10-31" }),
|
|
line({ id: "b" }),
|
|
])[0].invoices[0];
|
|
expect(mixed).toMatchObject({ driverId: "", mixedAssignment: true });
|
|
});
|
|
|
|
it("orders invoices newest first and finds the invoice PDF", () => {
|
|
const [company] = groupDeliveryPool([
|
|
line({ id: "a", customer_invoice_number: "INV1", invoiced_date: "2026-10-30" }),
|
|
line({ id: "b", customer_invoice_number: "INV2", invoiced_date: "2026-10-31", invoice_pdf: "inv2_abc.pdf" }),
|
|
]);
|
|
expect(company.invoices.map((i) => i.invoiceNumber)).toEqual(["INV2", "INV1"]);
|
|
expect(company.invoices[0].invoicePdf).toEqual({ lineId: "b", filename: "inv2_abc.pdf" });
|
|
});
|
|
});
|
|
|
|
describe("deliveryAssignmentState", () => {
|
|
const today = "2026-10-30";
|
|
it("covers every state", () => {
|
|
expect(deliveryAssignmentState({ driverId: "", assignedAt: "", mixedAssignment: false }, today)).toBe("unassigned");
|
|
expect(deliveryAssignmentState({ driverId: "d", assignedAt: today, mixedAssignment: false }, today)).toBe("today");
|
|
expect(deliveryAssignmentState({ driverId: "d", assignedAt: "2026-10-29", mixedAssignment: false }, today)).toBe("overdue");
|
|
expect(deliveryAssignmentState({ driverId: "d", assignedAt: "2026-11-02", mixedAssignment: false }, today)).toBe("future");
|
|
expect(deliveryAssignmentState({ driverId: "", assignedAt: "", mixedAssignment: true }, today)).toBe("mixed");
|
|
});
|
|
});
|
|
|
|
describe("filterDeliveryPool", () => {
|
|
const groups = groupDeliveryPool([
|
|
line({ id: "a", customer_invoice_number: "INV1", delivery_driver: "d1", delivery_assigned_at: "2026-10-30" }),
|
|
line({ id: "b", customer_invoice_number: "INV2", description: "Hex bolt" }),
|
|
line({ id: "c", customer_invoice_number: "INV3", company_id: "c2", company_name: "Bolt Co" }),
|
|
]);
|
|
|
|
it("matches company, invoice and line text", () => {
|
|
expect(filterDeliveryPool(groups, "hex", "all")[0].invoices.map((i) => i.invoiceNumber)).toEqual(["INV2"]);
|
|
expect(filterDeliveryPool(groups, "bolt co", "all").map((c) => c.name)).toEqual(["Bolt Co"]);
|
|
expect(filterDeliveryPool(groups, "inv3", "all")).toHaveLength(1);
|
|
});
|
|
|
|
it("filters by driver and drops empty companies", () => {
|
|
expect(filterDeliveryPool(groups, "", "d1").map((c) => c.invoices.length)).toEqual([1]);
|
|
const unassigned = filterDeliveryPool(groups, "", "unassigned");
|
|
expect(unassigned.flatMap((c) => c.invoices.map((i) => i.invoiceNumber)).sort()).toEqual(["INV2", "INV3"]);
|
|
});
|
|
});
|