Rust discount function (one automatic discount evaluating all offers in priority order, parity with the Odoo engine), React Router admin with Polaris, theme app extension and app proxy pages, webhooks incl. GDPR, managed-pricing limits, and a Docker Compose stack (Postgres + Caddy HTTPS) for a single VPS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
3.2 KiB
JavaScript
54 lines
3.2 KiB
JavaScript
// 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);
|