Check every amount on the page, in all three currencies, against the book or an approved string
The euro-only check searched the static USD page for euro amounts and found none; this one reads the per-currency and per-band attributes too, so an amount typed in the script in any currency fails.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+26
−6
test/pricing-pricebook.test.mjs+26 −6
diff --git a/test/pricing-pricebook.test.mjs b/test/pricing-pricebook.test.mjs| index 55782ff..1073141 100644 |
| --- a/test/pricing-pricebook.test.mjs |
| +++ b/test/pricing-pricebook.test.mjs |
| @@ -159,12 +159,32 @@ test('a page built from a draft book says so on its first line, and the check re |
| execFileSync('node', [build], { stdio: 'pipe', env: envWith('1') }); |
| }); |
| -test('no euro amount on the page is typed in the script: each comes from a string or the book', () => { |
| - const stripped = page.replace(/data-(currency|band)-values="[^"]*"/g, ''); |
| - for (const m of stripped.matchAll(/€[0-9][0-9.,]*/g)) { |
| - const amount = m[0]; |
| - const inBook = Object.values(book.plans).some((p) => Object.values(p.annual_unit_minor.eur).some((minor) => money('€', minor) === amount)); |
| +test('no amount on the page, in any currency, is typed in the script: each comes from the book or an approved string', () => { |
| + // Every priced value the page carries, static text and the per-currency |
| + // and per-band attributes alike, in all three currencies: each amount |
| + // in it is a book amount (an annual price at some band, a per-member |
| + // share of one, an overage rate or floor) or sits in an approved string. |
| + // A micro amount as the page may spell it: as short as it is exact, or |
| + // to two, three or four decimals (floors to two, rates to three or four). |
| + const micro = (sym, m) => [(m / 1e6).toString(), (m / 1e6).toFixed(2), (m / 1e6).toFixed(3), (m / 1e6).toFixed(4)].map((t) => sym + t); |
| + const rates = (code) => Object.values(book.overage[code]).flatMap((m) => micro(sym[code.toUpperCase()], m)); |
| + const annual = (code) => book.plans.flatMap((p) => Object.values(p.annual_unit_minor[code]).map((minor) => money(sym[code.toUpperCase()], minor))); |
| + const known = Object.fromEntries(book.currencies.map((c) => [c.code.toUpperCase(), new Set([...annual(c.code), ...rates(c.code)])])); |
| + const candidates = []; |
| + for (const m of page.replace(/data-(currency|band)-values="[^"]*"/g, '').matchAll(/[€$£][0-9][0-9.,]*/g)) candidates.push(m[0]); |
| + for (const v of valuesOf(page)) for (const [cur, text] of Object.entries(v)) for (const m of String(text).matchAll(/[€$£][0-9][0-9.,]*/g)) candidates.push(m[0]); |
| + for (const v of bandValuesOf(page)) for (const perBand of Object.values(v)) for (const text of Object.values(perBand)) for (const m of String(text).matchAll(/[€$£][0-9][0-9.,]*/g)) candidates.push(m[0]); |
| + assert.ok(candidates.length > 20, `found ${candidates.length} amounts to check`); |
| + const bySymbol = { '€': 'EUR', '$': 'USD', '£': 'GBP' }; |
| + for (const amount of candidates) { |
| + const code = bySymbol[amount[0]]; |
| + const inBook = known[code].has(amount) || known[code].has(amount.replace(/\.0+$/, '')); |
| const inStrings = strings.some((s) => s.text.includes(amount)); |
| - assert.ok(inBook || inStrings, amount + ' is on the page and in neither the book nor an approved string'); |
| + const share = (() => { |
| + // A per-member share of an annual price shown to two decimals. |
| + const n = Number(amount.slice(1)); |
| + return book.plans.some((p) => Object.values(p.annual_unit_minor[code.toLowerCase()]).some((minor) => Math.abs(minor / 100 - n) < 0.005 || Math.abs(minor / 100 / 12 - n) < 0.005)); |
| + })(); |
| + assert.ok(inBook || inStrings || share, amount + ' (' + code + ') is on the page and in neither the book nor an approved string'); |
| } |
| }); |