Add bundled TTF fonts, page geometry, column allocator and render model

Static TTF fonts (IBM Plex Sans/Mono, Noto Devanagari/Kannada) with a
fontkit-generated manifest, a verifier and OFL licences. Every family
registers a normal and italic source so fallback stacks cannot throw.
Adds A4/Letter geometry with margin presets, an exact column allocator,
deterministic Indian-grouping money and date formatting, text sanitising
and a pure buildRenderModel.
This commit is contained in:
2026-10-04 04:38:03 +05:30
parent fbed18a0bd
commit 92ba3d9590
40 changed files with 2162 additions and 49 deletions
+256
View File
@@ -0,0 +1,256 @@
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: "",
};
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,
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 });
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", 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 });
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).toMatch(/reverse charge/i);
expect(m.signature).toMatchObject({ name: "A. Person", designation: "Proprietor" });
});
});
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();
});
});