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>
50 lines
2.5 KiB
JavaScript
50 lines
2.5 KiB
JavaScript
// The optimised engine must give exactly the same applications as the reference port (which passes the
|
|
// Odoo vectors) on random carts and offers, including ties, overlaps, zero-discount uses and max uses.
|
|
import { test, expect } from 'vitest';
|
|
import * as fast from '../engine.js';
|
|
import * as ref from './reference/bundle-engine.reference.js';
|
|
|
|
function rng(seed) {
|
|
let s = seed >>> 0;
|
|
return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 2 ** 32; };
|
|
}
|
|
|
|
function randomCase(r) {
|
|
const pick = (n) => Math.floor(r() * n);
|
|
const nLines = 1 + pick(8);
|
|
const lines = Array.from({ length: nLines }, (_, i) => ({
|
|
id: `l${i}`, variantId: `v${pick(6)}`, productId: `p${pick(4)}`,
|
|
collectionIds: [`c${pick(3)}`, ...(r() < 0.3 ? [`c${pick(3)}`] : [])],
|
|
unitPrice: [1, 2, 2, 5, 7.5, 10, 10, 19.99][pick(8)], quantity: 1 + pick(4),
|
|
}));
|
|
const scope = () => ({ variantIds: new Set(r() < 0.3 ? [`v${pick(6)}`] : []),
|
|
productIds: new Set(r() < 0.4 ? [`p${pick(4)}`] : []), collectionIds: new Set(r() < 0.7 ? [`c${pick(3)}`] : []) });
|
|
const dts = ['percent', 'amount', 'fixed_price'];
|
|
const offers = Array.from({ length: 1 + pick(4) }, (_, i) => {
|
|
const type = ['fixed', 'mix', 'bxgy'][pick(3)];
|
|
const o = { id: `o${i}`, type, maxUses: pick(3), discountType: dts[pick(3)], value: [0, 1, 5, 10, 20, 50, 100][pick(7)] };
|
|
if (type === 'fixed') o.lines = Array.from({ length: 1 + pick(3) }, () => (r() < 0.5
|
|
? { variantId: `v${pick(6)}`, quantity: 1 + pick(2) } : { productId: `p${pick(4)}`, quantity: 1 + pick(2) }));
|
|
if (type === 'mix') { o.qty = 1 + pick(4); o.scope = scope(); }
|
|
if (type === 'bxgy') { o.buyQty = 1 + pick(3); o.getQty = 1 + pick(2); o.getPercent = [0, 50, 100][pick(3)];
|
|
o.buyScope = scope(); o.getScope = scope(); }
|
|
return o;
|
|
});
|
|
return { lines, offers };
|
|
}
|
|
|
|
const summary = (apps) => apps.map((a) => ({ o: a.offerId, d: a.discount, u: a.units.map((u) => `${u.lineId}:${u.price}`) }));
|
|
|
|
test('optimised engine == reference engine on 5000 random cases', () => {
|
|
const r = rng(20261013);
|
|
for (let i = 0; i < 5000; i += 1) {
|
|
const { lines, offers } = randomCase(r);
|
|
const a = fast.applyOffers(fast.toUnits(lines), offers);
|
|
const b = ref.applyOffers(ref.toUnits(lines), offers);
|
|
expect(summary(a), `case ${i}`).toEqual(summary(b));
|
|
const ta = [...fast.allocateByLine(a)].map(([k, v]) => [k, v.toFixed(9)]);
|
|
const tb = [...ref.allocateByLine(b)].map(([k, v]) => [k, v.toFixed(9)]);
|
|
expect(ta, `allocation case ${i}`).toEqual(tb);
|
|
}
|
|
});
|