import { describe, it, expect, afterEach } from "vitest"; import prisma from "../config/database"; import { createInvoice, getInvoiceStats } from "../services/invoices.service"; import { createOffer } from "../services/offers.service"; // These tests prove that DRAFT documents (a status introduced by the DB-drafts // feature) are NOT counted in period reporting aggregations. A draft is not a // real financial document, so its VAT/amount/created-count must be excluded. // // They hit the real `app_test` DB (per the suite convention) via the service // layer directly. Every row we create is tracked and removed in afterEach so we // leave the DB clean, and we reset the touched number sequences so a finalized // (issued/active) doc created here doesn't leak a consumed number into other // files. const invoiceIds: number[] = []; const offerIds: number[] = []; afterEach(async () => { for (const id of invoiceIds) await prisma.invoices.deleteMany({ where: { id } }); for (const id of offerIds) await prisma.quotations.deleteMany({ where: { id } }); invoiceIds.length = 0; offerIds.length = 0; await prisma.number_sequences.deleteMany({ where: { type: { in: ["invoice", "offer"] } }, }); }); describe("drafts excluded from invoice monthly stats (VAT)", () => { it("a draft invoice's VAT is NOT in vat_month while an issued one IS", async () => { // Pick a far-future month so no seeded/other-test invoices fall in it and // the totals are deterministic. issue_date drives the stats window. const year = 2099; const month = 7; const issueDate = `${year}-0${month}-15`; // Single CZK line, 21% VAT: base 1000, VAT 210. Identical on both docs so // the only difference between "draft excluded" and "issued counted" is the // status filter under test. const item = { quantity: 1, unit_price: 1000, vat_rate: 21 }; const draft = await createInvoice({ status: "draft", currency: "CZK", apply_vat: true, vat_rate: 21, issue_date: issueDate, items: [item], }); invoiceIds.push(draft.id); expect(draft.status).toBe("draft"); expect(draft.invoice_number).toBeNull(); // Sanity: with ONLY the draft present, vat_month must be empty (draft excluded). const draftOnly = await getInvoiceStats(month, year); expect(draftOnly.vat_month).toEqual([]); expect(draftOnly.vat_month_czk).toBe(0); const issued = await createInvoice({ status: "issued", currency: "CZK", apply_vat: true, vat_rate: 21, issue_date: issueDate, items: [item], }); invoiceIds.push(issued.id); expect(issued.status).toBe("issued"); // Now the issued invoice's VAT (210 CZK) must be counted — and ONLY that // one, not the draft's (which would double it to 420 if drafts leaked in). const stats = await getInvoiceStats(month, year); const czk = stats.vat_month.find((v) => v.currency === "CZK"); expect(czk?.amount).toBe(210); expect(stats.vat_month_czk).toBe(210); }); }); describe("drafts excluded from quotations created-this-month count", () => { it("a draft offer is NOT counted; an active one IS", async () => { // Mirror the dashboard route's exact aggregation: count quotations created // in the window, excluding drafts. Use a tight window around `now` (the // route uses the current month) and assert the delta this test introduces. const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 1); const countNonDraft = () => prisma.quotations.count({ where: { status: { not: "draft" }, created_at: { gte: monthStart, lt: monthEnd }, }, }); const before = await countNonDraft(); const draft = await createOffer({ status: "draft" }); if ("error" in draft) throw new Error(`createOffer failed: ${draft.error}`); offerIds.push(draft.id); expect(draft.status).toBe("draft"); // The draft must not move the non-draft count. expect(await countNonDraft()).toBe(before); const active = await createOffer({ status: "active" }); if ("error" in active) throw new Error(`createOffer failed: ${active.error}`); offerIds.push(active.id); // The active offer is a real document → the count goes up by exactly 1. expect(await countNonDraft()).toBe(before + 1); }); });