// Checks the JS engine port against spec/test-vectors.json (generated from the Odoo engine). // Run: node engine/tests/run-vectors.mjs (also part of npm test) import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { applyOffers, allocateByLine, largestRemainderCents, toUnits } from '../engine.js'; const here = dirname(fileURLToPath(import.meta.url)); const { vectors } = JSON.parse(readFileSync(join(here, '..', '..', 'spec', 'test-vectors.json'), 'utf8')); const scope = (s) => ({ variantIds: new Set(s.variant_ids || []), productIds: new Set(s.product_ids || []), collectionIds: new Set(s.collection_ids || []) }); const toOffer = (o) => ({ id: o.id, type: o.type, maxUses: o.max_uses || 0, discountType: o.discount_type, value: o.value, lines: (o.lines || []).map((l) => ({ variantId: l.variant_id, productId: l.product_id, quantity: l.quantity })), qty: o.qty, scope: o.scope && scope(o.scope), buyQty: o.buy_qty, getQty: o.get_qty, getPercent: o.get_percent, buyScope: o.buy_scope && scope(o.buy_scope), getScope: o.get_scope && scope(o.get_scope), }); const close = (a, b) => Math.abs(a - b) < 1e-6; let failed = 0; for (const v of vectors) { const lines = v.cart.lines.map((l) => ({ id: l.id, variantId: l.variant_id, productId: l.product_id, collectionIds: l.collection_ids, unitPrice: l.unit_price, quantity: l.quantity })); const apps = applyOffers(toUnits(lines), v.offers.map(toOffer)); const totals = allocateByLine(apps); const allocations = []; for (const o of v.offers) { const items = lines.map((l) => [l.id, totals.get(o.id + '\u0000' + l.id)]).filter(([, a]) => a !== undefined); const rounded = largestRemainderCents(items.map(([, a]) => a)); items.forEach(([lineId, a], i) => allocations.push({ offer_id: o.id, line_id: lineId, amount_exact: a, amount: rounded[i] })); } const exp = v.expected; const errors = []; if (apps.length !== exp.applications.length) errors.push(`applications ${apps.length} != ${exp.applications.length}`); apps.forEach((a, i) => { const e = exp.applications[i]; if (!e) return; if (a.offerId !== e.offer_id || !close(a.discount, e.discount)) errors.push(`app ${i}: ${a.offerId}/${a.discount} != ${e.offer_id}/${e.discount}`); const used = a.units.map((u) => u.lineId); if (JSON.stringify(used) !== JSON.stringify(e.line_ids_used)) errors.push(`app ${i} units ${used} != ${e.line_ids_used}`); }); if (allocations.length !== exp.allocations.length) errors.push(`allocations ${allocations.length} != ${exp.allocations.length}`); allocations.forEach((a, i) => { const e = exp.allocations[i]; if (!e) return; if (a.offer_id !== e.offer_id || a.line_id !== e.line_id || !close(a.amount_exact, e.amount_exact) || !close(a.amount, e.amount)) errors.push(`alloc ${i}: ${JSON.stringify(a)} != ${JSON.stringify(e)}`); }); if (errors.length) { failed += 1; console.log(`FAIL ${v.name}\n ` + errors.join('\n ')); } else console.log(`ok ${v.name}`); } console.log(failed ? `\n${failed} of ${vectors.length} failed` : `\nall ${vectors.length} vectors pass`); process.exit(failed ? 1 : 0);