WFDASH-WEBAPP/src/lib/pdf/format.test.ts

157 lines
4.7 KiB
TypeScript
Raw Normal View History

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);
});
});