carlos / carloku Public

Render the pricing page from the export before the draft check, and refuse a floor that is not whole cents

The build and check scripts ran the draft check on whatever page was on
disk, so a stale page could be published past a newer export; both now
render first. The renderer's money() threw nothing on a fractional minor
unit and would have printed "$1.20.5" for such a floor; it now refuses it.
Two tests pin the script order and the refusal (21 pass).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 402623181cabfeec4a2f54ecb9115d3bd7cccc73 parent c12318d
3 files changed, +23 −3
  • copy-review/pricing/build.mjs +1 −0
  • package.json +2 −2
  • test/pricing-pricebook.test.mjs +20 −1
diff --git a/copy-review/pricing/build.mjs b/copy-review/pricing/build.mjs
index a6e62c4..7ca92c2 100644
--- a/copy-review/pricing/build.mjs
+++ b/copy-review/pricing/build.mjs
@@ -39,6 +39,7 @@ 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") : "");
};
diff --git a/package.json b/package.json
index 8f0e8a8..1a0cfd2 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,9 @@
"private": true,
"type": "module",
"scripts": {
- "build": "node hack/check-pricebook.mjs && eleventy",
+ "build": "node copy-review/pricing/build.mjs && node hack/check-pricebook.mjs && eleventy",
"serve": "eleventy --serve",
- "check": "eleventy && node hack/check-docs.mjs && node hack/check-pricebook.mjs && node --test test/*.test.mjs"
+ "check": "node copy-review/pricing/build.mjs && node hack/check-pricebook.mjs && eleventy && node hack/check-docs.mjs && node --test test/*.test.mjs"
},
"devDependencies": {
"@11ty/eleventy": "^3.0.0",
diff --git a/test/pricing-pricebook.test.mjs b/test/pricing-pricebook.test.mjs
index d9ccc88..f585b03 100644
--- a/test/pricing-pricebook.test.mjs
+++ b/test/pricing-pricebook.test.mjs
@@ -165,6 +165,17 @@ test('the controls carry the five approved strings, and every country option its
assert.ok(page.includes('data-pricing-currency data-default-currency="USD"'));
});
+test('the build renders the page from the export before the draft check, so a stale page cannot be published past a newer export', () => {
+ const scripts = JSON.parse(readFileSync(new URL('package.json', root), 'utf8')).scripts;
+ for (const name of ['build', 'check']) {
+ const steps = scripts[name].split('&&').map((s) => s.trim());
+ const render = steps.findIndex((s) => s === 'node copy-review/pricing/build.mjs');
+ const check = steps.findIndex((s) => s === 'node hack/check-pricebook.mjs');
+ const eleventy = steps.findIndex((s) => s === 'eleventy');
+ assert.ok(render >= 0 && check > render && eleventy > check, `${name}: ${scripts[name]}`);
+ }
+});
+
test('a page built from a draft book says so on its first line, and the check refuses it unless a review build is asked for', () => {
const marked = page.startsWith(`<!-- PRICE BOOK ${book.price_book} IS ${book.status.toUpperCase()}: not approved for sale, not for publication -->`);
assert.equal(marked, book.status !== 'approved');
@@ -179,7 +190,7 @@ test('a page built from a draft book says so on its first line, and the check re
// The ordinary build runs the check too, so a draft page cannot be
// published by `npm run build` any more than by `npm run check`.
const scripts = JSON.parse(readFileSync(new URL('package.json', root), 'utf8')).scripts;
- assert.ok(scripts.build.startsWith('node hack/check-pricebook.mjs && '), scripts.build);
+ assert.ok(scripts.build.includes('node hack/check-pricebook.mjs && eleventy'), scripts.build);
assert.ok(scripts.check.includes('node hack/check-pricebook.mjs'), scripts.check);
// A refused build still wrote the preview, for review.
assert.ok(readFileSync(new URL('copy-review/pricing/preview/index.html', root), 'utf8').includes('data-pricing-currency'));
@@ -232,6 +243,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".
+ 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');
+ for (const cur of Object.keys(book.overage)) assert.equal(book.overage[cur].invoice_floor_micro % 10000, 0, cur + ' floor is whole cents');
+});
+
test('a figure is carried only as a whole number token, grouping commas on either side included', () => {
for (const [text, figure, want] of [
['Minimum 10 members', '10', true],