import { describe, it, expect, afterEach } from "vitest"; import prisma from "../config/database"; import { previewIssuedOrderNumber, previewInvoiceNumber, previewOfferNumber, } from "../services/numbering.service"; import { createIssuedOrder, updateIssuedOrder, deleteIssuedOrder, } from "../services/issued-orders.service"; import { createInvoice, updateInvoice, deleteInvoice, } from "../services/invoices.service"; import { createOffer, updateOffer, deleteOffer, } from "../services/offers.service"; // Track every row we create so the suite leaves the (real) app_test DB clean. const issuedOrderIds: number[] = []; const invoiceIds: number[] = []; const offerIds: number[] = []; afterEach(async () => { for (const id of issuedOrderIds) await prisma.issued_orders.deleteMany({ where: { id } }); for (const id of invoiceIds) await prisma.invoices.deleteMany({ where: { id } }); for (const id of offerIds) await prisma.quotations.deleteMany({ where: { id } }); issuedOrderIds.length = 0; invoiceIds.length = 0; offerIds.length = 0; // Reset every sequence this suite may have touched so preview values are // stable across tests and we don't leak consumed numbers into other files. await prisma.number_sequences.deleteMany({ where: { type: { in: ["issued_order", "invoice", "offer"] } }, }); }); /* -------------------------------------------------------------------------- */ /* Issued orders */ /* -------------------------------------------------------------------------- */ describe("issued-order drafts — deferred numbering", () => { it("a draft has no number and does NOT consume the sequence", async () => { const before = (await previewIssuedOrderNumber()).number; const draft = await createIssuedOrder({}); // defaults to draft if ("error" in draft) throw new Error(`createIssuedOrder failed: ${draft.error}`); issuedOrderIds.push(draft.id); expect(draft.status).toBe("draft"); expect(draft.po_number).toBeNull(); // Preview is unchanged → the sequence was not consumed. const after = (await previewIssuedOrderNumber()).number; expect(after).toBe(before); }); it("finalizing a draft (draft -> sent) assigns the next sequence number", async () => { const expected = (await previewIssuedOrderNumber()).number; const draft = await createIssuedOrder({}); if ("error" in draft) throw new Error(`createIssuedOrder failed: ${draft.error}`); issuedOrderIds.push(draft.id); const res = await updateIssuedOrder(draft.id, { status: "sent" }); expect("error" in res).toBe(false); const row = await prisma.issued_orders.findUnique({ where: { id: draft.id }, }); expect(row?.status).toBe("sent"); expect(row?.po_number).toBe(expected); expect("po_number" in res && res.po_number).toBe(expected); }); it("finalizing is idempotent — re-finalizing does not re-number", async () => { const draft = await createIssuedOrder({}); if ("error" in draft) throw new Error(`createIssuedOrder failed: ${draft.error}`); issuedOrderIds.push(draft.id); await updateIssuedOrder(draft.id, { status: "sent" }); const first = ( await prisma.issued_orders.findUnique({ where: { id: draft.id } }) )?.po_number; const seqAfterFirst = await prisma.number_sequences.findFirst({ where: { type: "issued_order" }, }); // draft -> sent is no longer valid (already sent); the number must not change. // Drive a second finalize-style update that keeps status sent. await updateIssuedOrder(draft.id, { status: "sent" }); const second = ( await prisma.issued_orders.findUnique({ where: { id: draft.id } }) )?.po_number; const seqAfterSecond = await prisma.number_sequences.findFirst({ where: { type: "issued_order" }, }); expect(second).toBe(first); expect(seqAfterSecond?.last_number).toBe(seqAfterFirst?.last_number); }); it("two drafts coexist with null numbers (no unique violation)", async () => { const a = await createIssuedOrder({}); const b = await createIssuedOrder({}); if ("error" in a) throw new Error(`createIssuedOrder failed: ${a.error}`); if ("error" in b) throw new Error(`createIssuedOrder failed: ${b.error}`); issuedOrderIds.push(a.id, b.id); expect(a.po_number).toBeNull(); expect(b.po_number).toBeNull(); }); it("deleting a draft releases nothing (no number was consumed)", async () => { const before = (await previewIssuedOrderNumber()).number; const draft = await createIssuedOrder({}); if ("error" in draft) throw new Error(`createIssuedOrder failed: ${draft.error}`); await deleteIssuedOrder(draft.id); const after = (await previewIssuedOrderNumber()).number; expect(after).toBe(before); }); }); /* -------------------------------------------------------------------------- */ /* Invoices */ /* -------------------------------------------------------------------------- */ describe("invoice drafts — deferred numbering", () => { it("a draft has no number and does NOT consume the sequence", async () => { const before = (await previewInvoiceNumber()).number; const draft = await createInvoice({}); invoiceIds.push(draft.id); expect(draft.status).toBe("draft"); expect(draft.invoice_number).toBeNull(); const after = (await previewInvoiceNumber()).number; expect(after).toBe(before); }); it("finalizing a draft (draft -> issued) assigns the next sequence number", async () => { const expected = (await previewInvoiceNumber()).number; const draft = await createInvoice({}); invoiceIds.push(draft.id); const res = await updateInvoice(draft.id, { status: "issued" }); expect("error" in res).toBe(false); const row = await prisma.invoices.findUnique({ where: { id: draft.id } }); expect(row?.status).toBe("issued"); expect(row?.invoice_number).toBe(expected); expect("invoice_number" in res && res.invoice_number).toBe(expected); }); it("finalizing twice does not re-number / double-consume", async () => { const draft = await createInvoice({}); invoiceIds.push(draft.id); await updateInvoice(draft.id, { status: "issued" }); const first = ( await prisma.invoices.findUnique({ where: { id: draft.id } }) )?.invoice_number; const seqAfterFirst = await prisma.number_sequences.findFirst({ where: { type: "invoice" }, }); // issued -> issued is a no-op status-wise; the number must stay put. await updateInvoice(draft.id, { status: "issued" }); const second = ( await prisma.invoices.findUnique({ where: { id: draft.id } }) )?.invoice_number; const seqAfterSecond = await prisma.number_sequences.findFirst({ where: { type: "invoice" }, }); expect(second).toBe(first); expect(seqAfterSecond?.last_number).toBe(seqAfterFirst?.last_number); }); it("two invoice drafts coexist with null numbers", async () => { const a = await createInvoice({}); const b = await createInvoice({}); invoiceIds.push(a.id, b.id); expect(a.invoice_number).toBeNull(); expect(b.invoice_number).toBeNull(); }); it("deleting a draft releases nothing", async () => { const before = (await previewInvoiceNumber()).number; const draft = await createInvoice({}); await deleteInvoice(draft.id); const after = (await previewInvoiceNumber()).number; expect(after).toBe(before); }); }); /* -------------------------------------------------------------------------- */ /* Offers */ /* -------------------------------------------------------------------------- */ describe("offer drafts — deferred numbering", () => { it("a draft has no number and does NOT consume the sequence", async () => { const before = await previewOfferNumber(); const draft = await createOffer({}); if ("error" in draft) throw new Error(`createOffer failed: ${draft.error}`); offerIds.push(draft.id); expect(draft.status).toBe("draft"); expect(draft.quotation_number).toBeNull(); const after = await previewOfferNumber(); expect(after).toBe(before); }); it("finalizing a draft (draft -> active) assigns the next sequence number", async () => { const expected = await previewOfferNumber(); const draft = await createOffer({}); if ("error" in draft) throw new Error(`createOffer failed: ${draft.error}`); offerIds.push(draft.id); const res = await updateOffer(draft.id, { status: "active" }); expect("error" in res).toBe(false); const row = await prisma.quotations.findUnique({ where: { id: draft.id } }); expect(row?.status).toBe("active"); expect(row?.quotation_number).toBe(expected); expect("quotation_number" in res && res.quotation_number).toBe(expected); }); it("finalizing twice does not re-number / double-consume", async () => { const draft = await createOffer({}); if ("error" in draft) throw new Error(`createOffer failed: ${draft.error}`); offerIds.push(draft.id); await updateOffer(draft.id, { status: "active" }); const first = ( await prisma.quotations.findUnique({ where: { id: draft.id } }) )?.quotation_number; const seqAfterFirst = await prisma.number_sequences.findFirst({ where: { type: "offer" }, }); // active -> active is a no-op; the number must stay put. await updateOffer(draft.id, { status: "active" }); const second = ( await prisma.quotations.findUnique({ where: { id: draft.id } }) )?.quotation_number; const seqAfterSecond = await prisma.number_sequences.findFirst({ where: { type: "offer" }, }); expect(second).toBe(first); expect(seqAfterSecond?.last_number).toBe(seqAfterFirst?.last_number); }); it("two offer drafts coexist with null numbers", async () => { const a = await createOffer({}); const b = await createOffer({}); if ("error" in a) throw new Error(`createOffer failed: ${a.error}`); if ("error" in b) throw new Error(`createOffer failed: ${b.error}`); offerIds.push(a.id, b.id); expect(a.quotation_number).toBeNull(); expect(b.quotation_number).toBeNull(); }); it("an offer created already-finalized (status active) numbers immediately", async () => { const expected = await previewOfferNumber(); const offer = await createOffer({ status: "active" }); if ("error" in offer) throw new Error(`createOffer failed: ${offer.error}`); offerIds.push(offer.id); expect(offer.quotation_number).toBe(expected); }); it("deleting a draft releases nothing", async () => { const before = await previewOfferNumber(); const draft = await createOffer({}); if ("error" in draft) throw new Error(`createOffer failed: ${draft.error}`); await deleteOffer(draft.id); const after = await previewOfferNumber(); expect(after).toBe(before); }); });