Files
Voiced/src/pdf/model/build.test.ts
T
xavierk cedf4839bd Replace the HTML preview with a worker-rendered PDF preview
Rendering moves into a module Web Worker behind a latest-wins RenderClient
with a watchdog and a handshake-based main-thread fallback. The preview
shows the real PDF through pdf.js at 2x with zoom, page indicator, audit
issues popover and a focus modal. A4/Letter and margin controls are frozen
into each invoice at issue and can be saved as the default (migration M3).
react-pdf and pdf.js are lazy-loaded; the main chunk shrinks from 1.8 MB
to 0.5 MB. The HTML twin and its styles are removed.
2026-10-04 05:47:19 +05:30

267 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Invoice> = {}, v: Partial<Settings> = {}, rest: Partial<BuildInput> = {}) =>
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();
});
});