import { describe, it, expect, afterEach } from "vitest"; import prisma from "../config/database"; import { generateIssuedOrderNumber, previewIssuedOrderNumber, releaseIssuedOrderNumber, } from "../services/numbering.service"; import { CreateIssuedOrderSchema } from "../schemas/issued-orders.schema"; import { computeIssuedOrderTotals, createIssuedOrder, getIssuedOrder, listIssuedOrders, updateIssuedOrder, deleteIssuedOrder, } from "../services/issued-orders.service"; import { renderIssuedOrderHtml } from "../routes/admin/issued-orders-pdf"; afterEach(async () => { await prisma.number_sequences.deleteMany({ where: { type: "issued_order" } }); }); describe("issued-order numbering", () => { it("generates sequential numbers", async () => { const year = new Date().getFullYear(); const a = await generateIssuedOrderNumber(year); const b = await generateIssuedOrderNumber(year); expect(Number(b.number)).toBe(Number(a.number) + 1); }); it("preview does not consume the sequence", async () => { const year = new Date().getFullYear(); const p1 = await previewIssuedOrderNumber(year); const p2 = await previewIssuedOrderNumber(year); expect(p1.number).toBe(p2.number); }); it("release frees only the highest number", async () => { const year = new Date().getFullYear(); await generateIssuedOrderNumber(year); // seq 1 await generateIssuedOrderNumber(year); // seq 2 const c = await generateIssuedOrderNumber(year); // seq 3 (current highest) const beforeRelease = (await previewIssuedOrderNumber(year)).number; // seq 4 // Releasing a number that is NOT the current highest is a no-op. await releaseIssuedOrderNumber(year, "00000000"); expect((await previewIssuedOrderNumber(year)).number).toBe(beforeRelease); // Releasing the current highest reclaims it. await releaseIssuedOrderNumber(year, c.number); expect((await previewIssuedOrderNumber(year)).number).toBe(c.number); }); }); describe("CreateIssuedOrderSchema", () => { it("coerces string form numbers and rejects an out-of-range VAT", () => { const ok = CreateIssuedOrderSchema.safeParse({ customer_id: "5", vat_rate: "21", items: [{ description: "X", quantity: "2", unit_price: "100" }], }); expect(ok.success).toBe(true); if (ok.success) expect(ok.data.customer_id).toBe(5); const bad = CreateIssuedOrderSchema.safeParse({ vat_rate: "200" }); expect(bad.success).toBe(false); }); }); const createdIds: number[] = []; const createdCustomerIds: number[] = []; afterEach(async () => { for (const id of createdIds) await prisma.issued_orders.deleteMany({ where: { id } }); for (const id of createdCustomerIds) await prisma.customers.deleteMany({ where: { id } }); createdIds.length = 0; createdCustomerIds.length = 0; }); async function makeCustomer() { const c = await prisma.customers.create({ data: { name: "Dodavatel s.r.o." }, }); createdCustomerIds.push(c.id); return c; } describe("computeIssuedOrderTotals (NET + VAT-on-top)", () => { it("adds VAT on top of net, rounded per line", () => { const t = computeIssuedOrderTotals( [ { quantity: 2, unit_price: 100, vat_rate: 21 }, { quantity: 1, unit_price: 50, vat_rate: 12 }, ], true, 21, ); expect(t.subtotal).toBe(250); expect(t.vat_amount).toBe(48); expect(t.total).toBe(298); }); it("zeroes VAT when apply_vat is false but keeps the net subtotal", () => { const t = computeIssuedOrderTotals( [{ quantity: 3, unit_price: 100, vat_rate: 21 }], false, 21, ); expect(t).toEqual({ subtotal: 300, vat_amount: 0, total: 300 }); }); it("falls back to the document rate when a line rate is null", () => { const t = computeIssuedOrderTotals( [{ quantity: 1, unit_price: 100, vat_rate: null }], true, 15, ); expect(t.vat_amount).toBe(15); }); }); describe("createIssuedOrder", () => { it("assigns a PO number, defaults to draft, stores items", async () => { const c = await makeCustomer(); const order = await createIssuedOrder({ customer_id: c.id, items: [ { description: "Materiál", quantity: 2, unit_price: 100, vat_rate: 21 }, ], }); createdIds.push(order.id); expect(order.po_number).toBeTruthy(); expect(order.status).toBe("draft"); const items = await prisma.issued_order_items.findMany({ where: { issued_order_id: order.id }, }); expect(items.length).toBe(1); expect(Number(items[0].unit_price)).toBe(100); }); }); describe("updateIssuedOrder status transitions", () => { it("allows draft -> sent and rejects draft -> completed", async () => { const order = await createIssuedOrder({}); createdIds.push(order.id); const ok = await updateIssuedOrder(order.id, { status: "sent" }); expect("error" in ok).toBe(false); const bad = await updateIssuedOrder(order.id, { status: "completed" }); expect("error" in bad && bad.error).toBe("invalid_transition"); }); it("locks items once confirmed", async () => { const order = await createIssuedOrder({ items: [{ description: "A", quantity: 1, unit_price: 10 }], }); createdIds.push(order.id); await updateIssuedOrder(order.id, { status: "sent" }); await updateIssuedOrder(order.id, { status: "confirmed" }); await updateIssuedOrder(order.id, { items: [{ description: "B", quantity: 9, unit_price: 99 }], }); const items = await prisma.issued_order_items.findMany({ where: { issued_order_id: order.id }, }); expect(items.length).toBe(1); expect(items[0].description).toBe("A"); }); }); describe("getIssuedOrder", () => { it("returns customer_name and valid_transitions", async () => { const c = await makeCustomer(); const order = await createIssuedOrder({ customer_id: c.id }); createdIds.push(order.id); const detail = await getIssuedOrder(order.id); expect(detail?.customer_name).toBe("Dodavatel s.r.o."); expect(detail?.valid_transitions).toContain("sent"); }); }); describe("deleteIssuedOrder", () => { it("deletes, cascades items, frees the latest number", async () => { const order = await createIssuedOrder({ items: [{ description: "X", quantity: 1, unit_price: 1 }], }); const before = order.po_number; await deleteIssuedOrder(order.id); expect( await prisma.issued_orders.findUnique({ where: { id: order.id } }), ).toBeNull(); expect( ( await prisma.issued_order_items.findMany({ where: { issued_order_id: order.id }, }) ).length, ).toBe(0); expect((await previewIssuedOrderNumber()).number).toBe(before); }); it("releases the number against the order's creation year, not the current year", async () => { const priorYear = new Date().getFullYear() - 1; // Allocate a real prior-year number (consumes that year's sequence: 0 -> 1) // so the PO number matches the prior year's current highest. const { number: poNumber } = await generateIssuedOrderNumber(priorYear); const order = await createIssuedOrder({ po_number: poNumber }); createdIds.push(order.id); // Backdate creation into the prior year so delete derives that year. await prisma.issued_orders.update({ where: { id: order.id }, data: { created_at: new Date(priorYear, 5, 1, 12, 0, 0) }, }); await deleteIssuedOrder(order.id); // The prior-year sequence must be decremented (1 -> 0). With the old // current-year bug it would stay at 1 (no current-year row to match). const seq = await prisma.number_sequences.findFirst({ where: { type: "issued_order", year: priorYear }, }); expect(seq?.last_number).toBe(0); }); }); describe("listIssuedOrders month filter", () => { it("returns only orders whose order_date is in the given month", async () => { const inMonth = await createIssuedOrder({ order_date: "2026-03-15" }); const outMonth = await createIssuedOrder({ order_date: "2026-04-15" }); createdIds.push(inMonth.id, outMonth.id); const res = await listIssuedOrders({ page: 1, limit: 50, skip: 0, sort: "id", order: "desc", month: 3, year: 2026, }); const ids = res.data.map((o) => o.id); expect(ids).toContain(inMonth.id); expect(ids).not.toContain(outMonth.id); }); }); describe("renderIssuedOrderHtml", () => { const order = { po_number: "26720001", order_date: new Date("2026-06-09T12:00:00"), delivery_date: null, currency: "CZK", apply_vat: true, vat_rate: 21, notes: "
Pozn
", delivery_terms: null, payment_terms: null, issued_by: null, }; const items = [ { description: "Materiál", item_description: "Detailní popis položky", quantity: 2, unit: "ks", unit_price: 100, vat_rate: 21, }, ]; it("renders the PO number, items, and both party names (PO direction)", () => { const html = renderIssuedOrderHtml( order, items, { name: "Dodavatel s.r.o." }, { company_name: "Naše firma" }, "cs", ); expect(html).toContain("26720001"); expect(html).toContain("Materiál"); // Optional item sub-line. expect(html).toContain("Detailní popis položky"); // PO direction: the customer record is the Dodavatel (supplier), our // company is the Odběratel (buyer). expect(html).toContain("Dodavatel s.r.o."); expect(html).toContain("Naše firma"); expect(html).toContain("Dodavatel"); expect(html).toContain("Odběratel"); }); it("strips script tags from notes", () => { const html = renderIssuedOrderHtml(order, items, null, null, "cs"); expect(html).not.toContain("