From bb0a17a6f3c1b1fffb54eab4863d6d42a627add7 Mon Sep 17 00:00:00 2001 From: Hoid Date: Sun, 1 Mar 2026 17:03:50 +0100 Subject: [PATCH] test: add 14 comprehensive template service tests Cover edge cases for invoice and receipt rendering: - Custom currency (invoice + receipt) - Multiple items with different tax rates - Zero tax rate - Missing optional fields - All optional fields present - Receipt with/without to field - Receipt paymentMethod - Empty items array (invoice + receipt) - Missing quantity (defaults to 1) - Missing unitPrice (defaults to 0) - Template list completeness check Total tests: 428 (was 414) --- src/__tests__/templates.test.ts | 182 ++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/src/__tests__/templates.test.ts b/src/__tests__/templates.test.ts index 52ca00b..9c5147b 100644 --- a/src/__tests__/templates.test.ts +++ b/src/__tests__/templates.test.ts @@ -54,4 +54,186 @@ describe("Template rendering", () => { expect(html).toContain("'"); expect(html).toContain("&"); }); + + // --- New tests --- + + it("invoice with custom currency uses $ instead of €", () => { + const html = renderTemplate("invoice", { + invoiceNumber: "INV-100", date: "2026-02-01", + from: { name: "US Corp" }, to: { name: "Client" }, + items: [{ description: "Service", quantity: 1, unitPrice: 50 }], + currency: "$", + }); + expect(html).toContain("$50.00"); + expect(html).not.toContain("€"); + }); + + it("invoice with multiple items calculates correct subtotal, tax, and total", () => { + const html = renderTemplate("invoice", { + invoiceNumber: "INV-200", date: "2026-02-01", + from: { name: "Seller" }, to: { name: "Buyer" }, + items: [ + { description: "Item A", quantity: 2, unitPrice: 100, taxRate: 20 }, // 200 + 40 tax + { description: "Item B", quantity: 1, unitPrice: 50, taxRate: 10 }, // 50 + 5 tax + { description: "Item C", quantity: 3, unitPrice: 30, taxRate: 0 }, // 90 + 0 tax + ], + }); + // Subtotal: 200 + 50 + 90 = 340 + expect(html).toContain("€340.00"); + // Tax: 40 + 5 + 0 = 45 + expect(html).toContain("€45.00"); + // Total: 385 + expect(html).toContain("€385.00"); + }); + + it("invoice with zero tax rate shows 0% and no tax amount", () => { + const html = renderTemplate("invoice", { + invoiceNumber: "INV-300", date: "2026-02-01", + from: { name: "Seller" }, to: { name: "Buyer" }, + items: [{ description: "Tax-free item", quantity: 1, unitPrice: 100, taxRate: 0 }], + }); + expect(html).toContain("0%"); + // Subtotal and total should be the same + expect(html).toContain("Subtotal: €100.00"); + expect(html).toContain("Tax: €0.00"); + expect(html).toContain("Total: €100.00"); + }); + + it("invoice with missing optional fields renders without errors", () => { + const html = renderTemplate("invoice", { + invoiceNumber: "INV-400", date: "2026-02-01", + from: { name: "Seller" }, to: { name: "Buyer" }, + items: [{ description: "Basic", quantity: 1, unitPrice: 10 }], + // no dueDate, no notes, no paymentDetails + }); + expect(html).toContain("INVOICE"); + expect(html).toContain("INV-400"); + expect(html).not.toContain("Due:"); + expect(html).not.toContain("Payment Details"); + expect(html).not.toContain("Notes"); + }); + + it("invoice with all optional fields renders them all", () => { + const html = renderTemplate("invoice", { + invoiceNumber: "INV-500", date: "2026-02-01", + dueDate: "2026-03-01", + from: { name: "Full Seller", address: "123 Main St", email: "seller@test.com", phone: "+1234", vatId: "AT123" }, + to: { name: "Full Buyer", address: "456 Oak Ave", email: "buyer@test.com", vatId: "DE456" }, + items: [{ description: "Premium", quantity: 1, unitPrice: 200, taxRate: 10 }], + currency: "€", + notes: "Please pay promptly", + paymentDetails: "IBAN: AT123456", + }); + expect(html).toContain("Due: 2026-03-01"); + expect(html).toContain("123 Main St"); + expect(html).toContain("seller@test.com"); + expect(html).toContain("VAT: AT123"); + expect(html).toContain("456 Oak Ave"); + expect(html).toContain("buyer@test.com"); + expect(html).toContain("VAT: DE456"); + expect(html).toContain("Please pay promptly"); + expect(html).toContain("IBAN: AT123456"); + }); + + it("receipt with custom currency uses £", () => { + const html = renderTemplate("receipt", { + receiptNumber: "R-100", date: "2026-02-01", + from: { name: "UK Shop" }, + items: [{ description: "Tea", amount: 3.50 }], + currency: "£", + }); + expect(html).toContain("£3.50"); + expect(html).not.toContain("€"); + }); + + it("receipt with paymentMethod shows it", () => { + const html = renderTemplate("receipt", { + receiptNumber: "R-200", date: "2026-02-01", + from: { name: "Shop" }, + items: [{ description: "Item", amount: 10 }], + paymentMethod: "Credit Card", + }); + expect(html).toContain("Paid via: Credit Card"); + }); + + it("receipt with to field shows customer name", () => { + const html = renderTemplate("receipt", { + receiptNumber: "R-300", date: "2026-02-01", + from: { name: "Shop" }, + to: { name: "Jane Doe" }, + items: [{ description: "Item", amount: 5 }], + }); + expect(html).toContain("Customer: Jane Doe"); + }); + + it("receipt without to field renders without error", () => { + const html = renderTemplate("receipt", { + receiptNumber: "R-400", date: "2026-02-01", + from: { name: "Shop" }, + items: [{ description: "Item", amount: 5 }], + }); + expect(html).toContain("Shop"); + expect(html).not.toContain("Customer:"); + }); + + it("invoice with empty items array renders with €0.00 total", () => { + const html = renderTemplate("invoice", { + invoiceNumber: "INV-600", date: "2026-02-01", + from: { name: "Seller" }, to: { name: "Buyer" }, + items: [], + }); + expect(html).toContain("Total: €0.00"); + }); + + it("receipt with empty items array renders with €0.00 total", () => { + const html = renderTemplate("receipt", { + receiptNumber: "R-500", date: "2026-02-01", + from: { name: "Shop" }, + items: [], + }); + expect(html).toContain("€0.00"); + }); + + it("invoice items with missing quantity defaults to 1", () => { + const html = renderTemplate("invoice", { + invoiceNumber: "INV-700", date: "2026-02-01", + from: { name: "Seller" }, to: { name: "Buyer" }, + items: [{ description: "Widget", unitPrice: 25 }], + }); + // quantity defaults to 1, so line total = 25 + expect(html).toContain("€25.00"); + }); + + it("invoice items with missing unitPrice defaults to 0", () => { + const html = renderTemplate("invoice", { + invoiceNumber: "INV-800", date: "2026-02-01", + from: { name: "Seller" }, to: { name: "Buyer" }, + items: [{ description: "Free item", quantity: 5 }], + }); + // unitPrice defaults to 0, line total = 0 + expect(html).toContain("Total: €0.00"); + }); + + it("template list contains both invoice and receipt with correct field definitions", () => { + expect(templates).toHaveProperty("invoice"); + expect(templates).toHaveProperty("receipt"); + expect(templates.invoice.name).toBe("Invoice"); + expect(templates.receipt.name).toBe("Receipt"); + // Invoice required fields + const invoiceRequired = templates.invoice.fields.filter(f => f.required).map(f => f.name); + expect(invoiceRequired).toContain("invoiceNumber"); + expect(invoiceRequired).toContain("date"); + expect(invoiceRequired).toContain("from"); + expect(invoiceRequired).toContain("to"); + expect(invoiceRequired).toContain("items"); + // Receipt required fields + const receiptRequired = templates.receipt.fields.filter(f => f.required).map(f => f.name); + expect(receiptRequired).toContain("receiptNumber"); + expect(receiptRequired).toContain("date"); + expect(receiptRequired).toContain("from"); + expect(receiptRequired).toContain("items"); + // Receipt 'to' is optional + const receiptTo = templates.receipt.fields.find(f => f.name === "to"); + expect(receiptTo?.required).toBe(false); + }); });