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
1.9 KiB
JavaScript
50 lines
1.9 KiB
JavaScript
// Turns the compact $app:configuration JSON (spec/offer-config.schema.json) into engine offers.
|
|
// Ids in the config are the numeric part of Shopify GIDs; the function compares numeric ids.
|
|
|
|
export function numericId(gid) {
|
|
if (gid == null) return '';
|
|
const s = String(gid);
|
|
const i = s.lastIndexOf('/');
|
|
return i >= 0 ? s.slice(i + 1) : s;
|
|
}
|
|
|
|
function toScope(sc) {
|
|
return {
|
|
variantIds: new Set(sc?.v || []),
|
|
productIds: new Set(sc?.p || []),
|
|
collectionIds: new Set(sc?.c || []),
|
|
};
|
|
}
|
|
|
|
/** Money values in the config are in shop currency; cart prices are in presentment currency. */
|
|
function convertValue(o, rate) {
|
|
if (o.dt === 'amount' || o.dt === 'fixed_price') return o.dv * rate;
|
|
return o.dv;
|
|
}
|
|
|
|
/**
|
|
* @param {object} cfg parsed configuration
|
|
* @param {{rate:number, country?:string, pastGuard?:boolean, guardAt?:string}} ctx
|
|
*/
|
|
export function offersFromConfig(cfg, ctx) {
|
|
const rate = ctx.rate > 0 ? ctx.rate : 1;
|
|
const out = [];
|
|
for (const o of cfg.offers || []) {
|
|
// Safety net for a late scheduler: once the guard time has passed, drop every offer that ends at or
|
|
// before it. The app republishes the configuration at each boundary; this only covers the gap.
|
|
if (ctx.pastGuard && o.e && cfg.g && o.e <= cfg.g) continue;
|
|
if (o.cc && o.cc.length && (!ctx.country || !o.cc.includes(ctx.country))) continue;
|
|
const base = { id: o.id, type: o.t, maxUses: o.mu || 0 };
|
|
if (o.t === 'fixed') {
|
|
out.push({ ...base, discountType: o.dt, value: convertValue(o, rate),
|
|
lines: (o.l || []).map((l) => ({ variantId: l.v, productId: l.v ? undefined : l.p, quantity: l.q })) });
|
|
} else if (o.t === 'mix') {
|
|
out.push({ ...base, discountType: o.dt, value: convertValue(o, rate), qty: o.q, scope: toScope(o.sc) });
|
|
} else if (o.t === 'bxgy') {
|
|
out.push({ ...base, buyQty: o.bq, getQty: o.gq, getPercent: o.gp,
|
|
buyScope: toScope(o.bs), getScope: toScope(o.gs) });
|
|
}
|
|
}
|
|
return out;
|
|
}
|