wacrm/src/lib/currency.test.ts
Arnas Donauskas c076b3cfe1
fix(currency): make formatCurrency total + add unit tests
deals.currency has no DB CHECK (only accounts.default_currency does),
so legacy rows, imports, or hand-edited data can hold a structurally
invalid code (e.g. "United States"). Intl.NumberFormat throws a
RangeError on such codes, which would crash any deal-card / contact
render that passes deal.currency straight through.

Guard formatCurrency with a try/catch that falls back to "CODE 1,234"
so a bad value degrades gracefully instead of taking down the view.
Add currency.test.ts locking in the defaults, the no-throw contract,
and formatCurrencyShort's symbol/fallback behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:10:56 +03:00

66 lines
2.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
CURRENCIES,
DEFAULT_CURRENCY,
formatCurrency,
formatCurrencyShort,
} from "./currency";
describe("formatCurrency", () => {
it("formats whole amounts with no minor units", () => {
// Use a non-breaking-space-tolerant check: Intl may insert NBSP.
const out = formatCurrency(1234, "USD");
expect(out).toContain("1,234");
expect(out).not.toContain(".00");
});
it("defaults to USD when no currency is given", () => {
expect(formatCurrency(10)).toBe(formatCurrency(10, DEFAULT_CURRENCY));
});
it("treats an empty-string currency as the default", () => {
expect(formatCurrency(10, "")).toBe(formatCurrency(10, DEFAULT_CURRENCY));
});
it("coerces non-finite values to 0", () => {
expect(formatCurrency(Number.NaN, "USD")).toContain("0");
});
it("renders a well-formed but unknown ISO code without throwing", () => {
// Intl is lenient here — it uses the code as the symbol.
const out = formatCurrency(1234, "ZZZ");
expect(out).toContain("ZZZ");
expect(out).toContain("1,234");
});
it("never throws on a structurally invalid code (no DB CHECK on deals.currency)", () => {
for (const bad of ["United States", "US", "USDD", "12", "u$d"]) {
expect(() => formatCurrency(1234, bad)).not.toThrow();
expect(formatCurrency(1234, bad)).toContain("1,234");
}
});
it("formats every offered currency without throwing", () => {
for (const c of CURRENCIES) {
expect(() => formatCurrency(1000, c.code)).not.toThrow();
}
});
});
describe("formatCurrencyShort", () => {
it("abbreviates millions and thousands with the currency symbol", () => {
expect(formatCurrencyShort(2_500_000, "USD")).toBe("$2.5M");
expect(formatCurrencyShort(3_400, "USD")).toBe("$3.4k");
expect(formatCurrencyShort(900, "USD")).toBe("$900");
});
it("uses the matching symbol for non-USD currencies", () => {
expect(formatCurrencyShort(1_000, "EUR")).toBe("€1.0k");
expect(formatCurrencyShort(1_000, "INR")).toBe("₹1.0k");
});
it("falls back to the code prefix for unknown currencies (no throw)", () => {
expect(formatCurrencyShort(1_000, "ZZZ")).toBe("ZZZ 1.0k");
});
});