Support zero- and three-decimal currencies on invoices
Node · Node · advanced · modification
Adds support for zero- and three-decimal currencies (JPY, JOD, KWD, BHD) and the new invoice formatter that renders line items and the total. Verified end to end on staging — line amounts, totals and rejection of unsupported currencies all behave.
A B2B billing service expanding into the Middle East and Japan. Amounts arrive from the ledger as integer minor units and are only ever converted at the display edge.
Requirements
- billing/currencies.js is the single source of truth for money display: amounts are stored as integer minor units, and converting one to a display amount must follow the minor-unit contract documented at the top of that file.
- This PR adds JOD, KWD, BHD and JPY to the supported set and introduces the invoice formatter. Invoices in every supported currency must render correct amounts.
- formatAmount(minorUnits, currency): minorUnits is an integer in [0, 10^12); reject non-integers, negatives, and unsupported currencies with a RangeError.
- formatInvoice(invoice) renders one line per invoice line item plus a TOTAL line; an invoice has a single currency and 1 to 1,000 line items, and line amounts are integers in [0, 10^9).
Files touched
- billing/currencies.js
- billing/invoice-format.js
--- billing/currencies.js
EUR: 2,
GBP: 2,
+ JOD: 3,
+ KWD: 3,
+ BHD: 3,
+ JPY: 0,
};
--- billing/invoice-format.js
+// billing/invoice-format.js
+
+const { isSupportedCurrency } = require("./currencies");
+
+function formatAmount(minorUnits, currency) {
+ if (!Number.isInteger(minorUnits) || minorUnits < 0 || minorUnits >= 1e12) {
+ throw new RangeError(`amount must be an integer of minor units in [0, 1e12), got ${minorUnits}`);
+ }
+ if (!isSupportedCurrency(currency)) {
+ throw new RangeError(`unsupported currency: ${currency}`);
+ }
+ const major = minorUnits / 100;
+ return `${major.toFixed(2)} ${currency}`;
+}
+
+function formatInvoice(invoice) {
+ const lines = invoice.lines.map(
+ (line) => `${line.description} ${formatAmount(line.amountMinor, invoice.currency)}`
+ );
+ const total = invoice.lines.reduce((sum, line) => sum + line.amountMinor, 0);