carlos / carloku Public

The money renderer is a module the test calls: a fractional minor unit is refused by moneyText, asserted by calling it, not by reading the source

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 01b62f718f1592fcc55126dc443c4bd0401505c7 parent 4026231
3 files changed, +20 −8
  • copy-review/pricing/build.mjs +2 −5
  • hack/money.mjs +10 −0
  • test/pricing-pricebook.test.mjs +8 −3
diff --git a/copy-review/pricing/build.mjs b/copy-review/pricing/build.mjs
index 7ca92c2..81d63ec 100644
--- a/copy-review/pricing/build.mjs
+++ b/copy-review/pricing/build.mjs
@@ -6,6 +6,7 @@
import fs from "node:fs";
import path from "node:path";
import { carriesToken } from "../../hack/token.mjs";
+import { moneyText } from "../../hack/money.mjs";
const here = path.dirname(new URL(import.meta.url).pathname);
const root = path.resolve(here, "..", "..");
const strings = JSON.parse(fs.readFileSync(path.join(here, "strings.json"), "utf8"));
@@ -38,11 +39,7 @@ const symbol = Object.fromEntries(book.currencies.map((c) => [c.code.toUpperCase
const plansByKey = Object.fromEntries(book.plans.map((pl) => [pl.key, pl]));
// money renders minor units as the approved strings do: "€24", "€8.40",
// "$1,575" is not needed (no price reaches a thousand).
-const money = (cur, minor) => {
- if (!Number.isInteger(minor)) throw new Error("not a whole number of minor units: " + cur + " " + minor);
- const major = Math.floor(minor / 100), rem = minor % 100;
- return symbol[cur] + major + (rem ? "." + String(rem).padStart(2, "0") : "");
-};
+const money = (cur, minor) => moneyText(symbol[cur], minor, cur);
// rate renders micro units exactly: at least three decimals, as the
// approved EUR rate strings are written ("€0.010 per hour"), and as many
// more as the figure needs ("$0.1272", "£0.0207"), never rounded. What the
diff --git a/hack/money.mjs b/hack/money.mjs
new file mode 100644
index 0000000..b08d135
--- /dev/null
+++ b/hack/money.mjs
@@ -0,0 +1,10 @@
+// moneyText renders a whole number of minor units as the approved strings
+// do: "€24", "€8.40". A minor amount that is not a whole number is refused
+// rather than rendered ("$1.20.5" was the alternative): the price book
+// carries whole cents, and a floor that is not would be a book error, not
+// a formatting case.
+export function moneyText(symbol, minor, label) {
+ if (!Number.isInteger(minor)) throw new Error("not a whole number of minor units: " + label + " " + minor);
+ const major = Math.floor(minor / 100), rem = minor % 100;
+ return symbol + major + (rem ? "." + String(rem).padStart(2, "0") : "");
+}
diff --git a/test/pricing-pricebook.test.mjs b/test/pricing-pricebook.test.mjs
index f585b03..ecacde7 100644
--- a/test/pricing-pricebook.test.mjs
+++ b/test/pricing-pricebook.test.mjs
@@ -8,6 +8,7 @@ import { execFileSync } from 'node:child_process';
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { carriesToken } from '../hack/token.mjs';
+import { moneyText } from '../hack/money.mjs';
const root = new URL('../', import.meta.url);
const book = JSON.parse(readFileSync(new URL('copy-review/pricing/pricebook.json', root), 'utf8'));
@@ -244,10 +245,14 @@ test('every priced element shows, as static text, exactly its own USD value, and
});
test('a floor that is not a whole number of cents is refused by the renderer, not shown as a malformed amount', () => {
- // The renderer's money() is the one place a floor is spelt; it throws on
- // a fractional minor unit rather than printing "$1.20.5".
+ // The renderer's money is moneyText, the one place a floor is spelt; it
+ // throws on a fractional minor unit rather than printing "$1.20.5", and
+ // the build's money() is that function with the currency's symbol.
+ assert.throws(() => moneyText('$', 120.5, 'USD'), /not a whole number of minor units/);
+ assert.equal(moneyText('$', 120, 'USD'), '$1.20');
+ assert.equal(moneyText('€', 2400, 'EUR'), '€24');
const src = readFileSync(new URL('copy-review/pricing/build.mjs', root), 'utf8');
- assert.ok(src.includes('if (!Number.isInteger(minor)) throw new Error("not a whole number of minor units'), 'money() refuses a fractional minor unit');
+ assert.ok(src.includes('const money = (cur, minor) => moneyText(symbol[cur], minor, cur);'), 'the build renders money through moneyText');
for (const cur of Object.keys(book.overage)) assert.equal(book.overage[cur].invoice_floor_micro % 10000, 0, cur + ' floor is whole cents');
});