WFDASH-WEBAPP/src/lib/pdf/format.test.ts
SJCoetz b3d9eeb5ce feat(pdf): add poSubtotal helper for PO PDF totals
Co-authored-by: Orca <help@stably.ai>
2026-08-06 11:54:23 +02:00

156 lines
4.7 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, it, expect } from "vitest";
import {
formatZAR,
formatDateLong,
addDays,
quoteSubtotal,
vatAmount,
poSubtotal,
QUOTE_VALID_DAYS,
effectiveQuoteDate,
effectiveValidityDays,
} from "./format";
describe("formatZAR", () => {
it("formats whole numbers with 2 decimals", () => {
expect(formatZAR(1234)).toBe("R 1 234.00");
});
it("groups thousands with spaces (en-ZA style)", () => {
expect(formatZAR(1234567.89)).toBe("R 1 234 567.89");
});
it("handles zero", () => {
expect(formatZAR(0)).toBe("R 0.00");
});
it("handles small decimals", () => {
expect(formatZAR(99.5)).toBe("R 99.50");
});
it("returns R 0.00 for non-finite numbers", () => {
expect(formatZAR(NaN)).toBe("R 0.00");
expect(formatZAR(Infinity)).toBe("R 0.00");
});
it("matches the existing sample PDF format", () => {
// From Quote-CNL-Q009638: Subtotal R 737.06, VAT R 110.56, Total R 847.62
expect(formatZAR(737.06)).toBe("R 737.06");
expect(formatZAR(110.56)).toBe("R 110.56");
expect(formatZAR(847.62)).toBe("R 847.62");
});
});
describe("formatDateLong", () => {
it("formats ISO date to '22 June 2026' style", () => {
expect(formatDateLong("2026-06-22T00:00:00Z")).toBe("22 June 2026");
});
it("returns empty string for empty input", () => {
expect(formatDateLong("")).toBe("");
});
it("returns the original string for unparseable input", () => {
expect(formatDateLong("not-a-date")).toBe("not-a-date");
});
});
describe("addDays", () => {
it("adds exactly N days, preserving the calendar date", () => {
expect(addDays("2026-06-22T00:00:00Z", 30)).toBe(
"2026-07-22T00:00:00.000Z"
);
});
it("handles month rollover", () => {
expect(addDays("2026-01-31T00:00:00Z", 1)).toBe(
"2026-02-01T00:00:00.000Z"
);
});
it("QUOTE_VALID_DAYS is 30 (matches sample PDF)", () => {
expect(QUOTE_VALID_DAYS).toBe(30);
});
});
describe("quoteSubtotal", () => {
it("sums qty * selling_price", () => {
expect(quoteSubtotal([{ quantity: 1, selling_price: 648.7 }])).toBeCloseTo(648.7);
expect(
quoteSubtotal([
{ quantity: 1, selling_price: 648.7 },
{ quantity: 1, selling_price: 88.36 },
])
).toBeCloseTo(737.06);
});
it("returns 0 for empty array", () => {
expect(quoteSubtotal([])).toBe(0);
});
});
describe("vatAmount", () => {
it("computes 15% VAT, rounded to 2dp", () => {
expect(vatAmount(737.06)).toBe(110.56);
});
it("returns 0 for 0 subtotal", () => {
expect(vatAmount(0)).toBe(0);
});
});
describe("effectiveQuoteDate", () => {
it("prefers quote_date when set", () => {
expect(
effectiveQuoteDate({
quote_date: "2026-06-01T00:00:00.000Z",
created: "2026-06-22T13:40:00.000Z",
})
).toBe("2026-06-01T00:00:00.000Z");
});
it("falls back to created when quote_date is empty", () => {
expect(
effectiveQuoteDate({
quote_date: "",
created: "2026-06-22T13:40:00.000Z",
})
).toBe("2026-06-22T13:40:00.000Z");
});
it("returns empty string when both are empty", () => {
expect(effectiveQuoteDate({ quote_date: "", created: "" })).toBe("");
});
});
describe("effectiveValidityDays", () => {
it("uses quote.validity_days when set to a number", () => {
expect(effectiveValidityDays({ validity_days: 60 })).toBe(60);
});
it("falls back to QUOTE_VALID_DAYS (30) when null", () => {
expect(effectiveValidityDays({ validity_days: null })).toBe(QUOTE_VALID_DAYS);
});
it("falls back to QUOTE_VALID_DAYS (30) when undefined", () => {
expect(effectiveValidityDays({})).toBe(QUOTE_VALID_DAYS);
});
});
describe("poSubtotal", () => {
it("sums order_qty × cost_price when order_qty is set", () => {
expect(poSubtotal([{ order_qty: 3, quantity: 1, cost_price: 100 }])).toBe(300);
});
it("falls back to quantity when order_qty is missing", () => {
expect(poSubtotal([{ quantity: 4, cost_price: 50 }])).toBe(200);
});
it("falls back to quantity when order_qty is 0 (blank treated as missing)", () => {
expect(poSubtotal([{ order_qty: 0, quantity: 2, cost_price: 75 }])).toBe(150);
});
it("sums across multiple items", () => {
expect(poSubtotal([
{ order_qty: 3, quantity: 1, cost_price: 100 },
{ order_qty: 5, quantity: 1, cost_price: 40 },
])).toBe(500);
});
it("rounds to 2 decimal places (avoids floating-point dust)", () => {
expect(poSubtotal([
{ quantity: 3, cost_price: 33.33 },
{ quantity: 3, cost_price: 33.33 },
{ quantity: 3, cost_price: 33.34 },
])).toBe(300);
});
it("returns 0 for an empty array", () => {
expect(poSubtotal([])).toBe(0);
});
it("treats non-numeric cost_price as 0", () => {
expect(poSubtotal([{ quantity: 5, cost_price: NaN }])).toBe(0);
});
});