import { describe, expect, it } from "vitest"; import type { BankAccount, Invoice, InvoiceItem, Settings } from "../../lib/types"; import { buildRenderModel, type BuildInput } from "./build"; import { bankFromSnapshot, vendorFromSnapshot } from "./vendor"; const vendor: Settings = { vendorName: "Test Vendor", vendorAddress: "13-B Type House,\r\nTest Road, Test Town\nBangalore - 560005", vendorEmail: "Sample@example.com", vendorPhone: "9000000000", vendorPan: "ABCDE1234F", vendorGstin: "", vendorStateCode: "29", logoPath: null, signaturePath: null, defaultBankId: 1, defaultTaxRate: 18, defaultTaxType: "cgst_sgst", paymentTermsDays: 30, currency: "INR", onboarded: true, theme: "g10", gstRegistration: "unregistered", defaultHsnSac: "", signatoryName: "", signatoryDesignation: "", renderPrefs: "", }; const bank: BankAccount = { id: 1, label: "Primary", bankName: "State Bank of India", accountName: "Test Vendor", accountNo: "000000000000", branch: "000001", ifsc: "TEST0000001", isDefault: true, }; const items: InvoiceItem[] = [ { description: "Voice-over\nsession A", mode: "rate", rate: 1200, unit: "hour", quantity: 2.5, amount: 3000, sortOrder: 0, hsnSac: "998397" }, { description: "", mode: "fixed", rate: 0, unit: "unit", quantity: 1, amount: 2500.5, sortOrder: 1 }, ]; const base: Invoice = { id: 1, number: "INV-0001", seriesId: 1, invoiceDate: "2026-09-15", dueDate: "2026-10-05", clientId: null, clientName: "Acme Pvt Ltd", clientAddress: "1 Main Road\r\nMumbai", clientGstin: "", poNumber: "", placeOfSupplyStateCode: "", subtotal: 5500.5, discount: 0, taxType: "none", taxRate: 0, cgstAmount: 0, sgstAmount: 0, igstAmount: 0, total: 5500.5, amountInWords: "Five Thousand Five Hundred Rupees and Fifty Paise Only", bankAccountId: 1, bankSnapshot: "", signaturePath: null, notes: "Thanks\r\n\r\nPay in 30 days", status: "issued", createdAt: "2026-09-15T00:00:00Z", updatedAt: "2026-09-15T00:00:00Z", docType: "invoice", reverseCharge: false, vendorSnapshot: "", snapshotOrigin: "", cancelledAt: null, cancelReason: "", archivedPdfSha256: null, renderPrefs: "", items, }; const make = (over: Partial = {}, v: Partial = {}, rest: Partial = {}) => buildRenderModel({ invoice: { ...base, ...over }, vendor: { ...vendor, ...v }, bank, logo: null, signatureDataUri: null, appVersion: "0.1.0", ...rest, }); describe("unregistered invoice", () => { const m = make(); it("is a plain Invoice without tax slots", () => { expect(m.docTitle).toBe("Invoice"); expect(m.docType).toBe("invoice"); expect(m.pdfTitle).toBe("Invoice INV-0001"); expect(m.slots).toEqual({ gstin: false, clientGstin: false, pos: false, hsn: true, taxRows: false, reverseCharge: false }); expect(m.totals).toMatchObject({ subtotalText: "₹5,500.50", discountText: null, taxableText: null, taxRows: [], totalText: "₹5,500.50", }); expect(m.supplier.gstin).toBeNull(); expect(m.supplier.showGstin).toBe(false); expect(m.client.posLabel).toBeNull(); expect(m.cancelled).toBe(false); expect(m.reverseCharge).toBeNull(); expect(m.logo).toBeNull(); }); it("formats and sanitises text", () => { expect(m.supplier.addressLines).toEqual(["13-B Type House,", "Test Road, Test Town", "Bangalore - 560005"]); expect(m.supplier.stateName).toBe("Karnataka"); expect(m.client.name).toBe("Acme Pvt Ltd"); expect(m.client.addressLines).toEqual(["1 Main Road", "Mumbai"]); expect(m.client.poNumber).toBeNull(); expect(m.notesLines).toEqual(["Thanks", "Pay in 30 days"]); expect(m.meta).toEqual({ numberLabel: "Invoice No.", number: "INV-0001", date: "15 Sept 2026", isoDate: "2026-09-15", dueDate: "05 Oct 2026" }); }); it("builds items with rate, quantity and em-dash fallback", () => { expect(m.items[0]).toEqual({ sno: 1, descriptionLines: ["Voice-over", "session A"], hsnSac: "998397", rateText: "₹1,200.00 per hour", qtyText: "2.5", amountText: "₹3,000.00", }); expect(m.items[1]).toEqual({ sno: 2, descriptionLines: ["—"], hsnSac: null, rateText: null, qtyText: "1", amountText: "₹2,500.50", }); }); it("labels the bank lines and defaults the signature to the vendor name", () => { expect(m.bank).toEqual({ accountName: "Test Vendor", lines: [ { label: "Bank", value: "State Bank of India" }, { label: "Account No.", value: "000000000000" }, { label: "Branch", value: "000001" }, { label: "IFSC", value: "TEST0000001" }, ], }); expect(m.signature).toEqual({ dataUri: null, name: "Test Vendor", designation: "" }); }); }); describe("CGST + SGST tax invoice", () => { const m = make( { docType: "tax_invoice", taxType: "cgst_sgst", taxRate: 5, discount: 500.5, cgstAmount: 125, sgstAmount: 125, total: 5250, clientGstin: "29ABCDE1234F1Z5", placeOfSupplyStateCode: "29", poNumber: "PO-77", reverseCharge: true, }, { vendorGstin: "29AAAAA0000A1Z5", gstRegistration: "regular", signatoryName: "A. Person", signatoryDesignation: "Proprietor" }, ); it("derives labels, rates and slots", () => { expect(m.docTitle).toBe("Tax Invoice"); expect(m.totals.taxRows).toEqual([ { label: "CGST", rateText: "2.5%", amountText: "₹125.00" }, { label: "SGST", rateText: "2.5%", amountText: "₹125.00" }, ]); expect(m.totals.discountText).toBe("₹500.50"); expect(m.totals.taxableText).toBe("₹5,000.00"); expect(m.totals.totalText).toBe("₹5,250.00"); expect(m.slots).toEqual({ gstin: true, clientGstin: true, pos: true, hsn: true, taxRows: true, reverseCharge: true }); expect(m.client.posLabel).toBe("29 – Karnataka"); expect(m.client.gstin).toBe("29ABCDE1234F1Z5"); expect(m.client.poNumber).toBe("PO-77"); expect(m.supplier.showGstin).toBe(true); expect(m.reverseCharge).toBe("Tax is payable on reverse charge: Yes"); expect(m.signature).toMatchObject({ name: "A. Person", designation: "Proprietor" }); }); }); describe("reverse charge line", () => { it("is always present on a tax invoice (Yes or No) and absent on a plain invoice", () => { expect(make({ docType: "tax_invoice", reverseCharge: false }).reverseCharge).toBe("Tax is payable on reverse charge: No"); expect(make({ docType: "tax_invoice", reverseCharge: true }).reverseCharge).toBe("Tax is payable on reverse charge: Yes"); expect(make({ docType: "invoice", reverseCharge: true }).reverseCharge).toBeNull(); }); }); describe("IGST", () => { const m = make({ docType: "tax_invoice", taxType: "igst", taxRate: 18, igstAmount: 990.09, total: 6490.59, placeOfSupplyStateCode: "27" }); it("has one IGST row at the full rate", () => { expect(m.totals.taxRows).toEqual([{ label: "IGST", rateText: "18%", amountText: "₹990.09" }]); expect(m.client.posLabel).toBe("27 – Maharashtra"); }); }); describe("UTGST supplier states", () => { it.each(["04", "26", "31", "35", "38"])("uses UTGST for supplier state %s", (code) => { const m = make({ docType: "tax_invoice", taxType: "cgst_sgst", taxRate: 18, cgstAmount: 1, sgstAmount: 1 }, { vendorStateCode: code }); expect(m.totals.taxRows.map((r) => r.label)).toEqual(["CGST", "UTGST"]); }); it("keeps SGST for ordinary states and Delhi/Puducherry", () => { for (const code of ["29", "07", "34", "01"]) { const m = make({ docType: "tax_invoice", taxType: "cgst_sgst", taxRate: 18 }, { vendorStateCode: code }); expect(m.totals.taxRows[1].label).toBe("SGST"); } }); it("resolves the supplier state name", () => { expect(make({}, { vendorStateCode: "04" }).supplier.stateName).toBe("Chandigarh"); }); }); describe("cancelled and logo", () => { it("flags cancelled invoices", () => { expect(make({ status: "cancelled" }).cancelled).toBe(true); expect(make({ status: "draft" }).cancelled).toBe(false); }); it("passes the logo and signature through, and keeps null when absent", () => { const logo = { dataUri: "data:image/png;base64,AA==", width: 200, height: 40 }; const m = make({}, {}, { logo, signatureDataUri: "data:image/png;base64,BB==" }); expect(m.logo).toEqual(logo); expect(m.signature.dataUri).toBe("data:image/png;base64,BB=="); expect(make().logo).toBeNull(); }); }); describe("snapshots", () => { it("lets the invoice's vendor snapshot override live settings", () => { const snap = JSON.stringify({ vendorName: "Old Name", vendorStateCode: "04", vendorGstin: "04AAAAA0000A1Z5", gstRegistration: "regular" }); const m = make({ vendorSnapshot: snap, docType: "tax_invoice", taxType: "cgst_sgst", taxRate: 12 }); expect(m.supplier.name).toBe("Old Name"); expect(m.supplier.stateCode).toBe("04"); expect(m.totals.taxRows[1].label).toBe("UTGST"); expect(m.supplier.showGstin).toBe(true); }); it("falls back to live settings for a corrupt snapshot", () => { expect(vendorFromSnapshot("{nope", vendor)).toBe(vendor); expect(vendorFromSnapshot(undefined, vendor)).toBe(vendor); expect(vendorFromSnapshot("", vendor)).toBe(vendor); }); it("parses the bank snapshot only when no bank is given", () => { const snap = JSON.stringify({ ...bank, bankName: "HDFC" }); expect(bankFromSnapshot(snap, null)?.bankName).toBe("HDFC"); expect(bankFromSnapshot(snap, bank)).toBe(bank); expect(bankFromSnapshot("{bad", null)).toBeNull(); expect(bankFromSnapshot("", null)).toBeNull(); const m = buildRenderModel({ invoice: { ...base, bankSnapshot: snap }, vendor, bank: null, logo: null, signatureDataUri: null, appVersion: "x" }); expect(m.bank?.lines[0]).toEqual({ label: "Bank", value: "HDFC" }); }); it("omits the bank block when there is no bank", () => { expect(make({}, {}, { bank: null }).bank).toBeNull(); }); });