@blamejs/blamejs-shop 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/checkout.js +125 -1
- package/lib/storefront.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.3.x
|
|
10
10
|
|
|
11
|
+
- v0.3.9 (2026-05-30) — **Automatic discounts now apply at checkout.** The automatic-discount rules you define in the admin console — an amount or percentage off a cart that meets a threshold — now reduce the order total at checkout. When a rule matches, the discount is applied (capped so it never exceeds the order subtotal) and shown in the quote the shopper pays; when no rule matches, or none are defined, the total is exactly as before, so a store that has not set up rules is unaffected. The admin screen for managing rules already shipped; this wires it through to the checkout quote and the charge. **Added:** *Automatic cart discounts at checkout* — When a cart matches a configured automatic-discount rule, the checkout reduces the order total by the rule's saving — capped to the order subtotal so the total is never negative — and the reduced amount is what the shopper is charged. With no matching rule, or no rules defined, the order total is unchanged, so the change stays invisible until you set rules up; any rule-engine error degrades to no discount rather than blocking checkout.
|
|
12
|
+
|
|
11
13
|
- v0.3.8 (2026-05-30) — **Update the vendored blamejs runtime to v0.14.6.** Refreshes the vendored blamejs runtime from v0.14.5 to v0.14.6, a maintenance update. The createApp request-lifecycle security stack the shop composes — CSRF, fetch-metadata, body-parser, cookies, rate limiting — is unchanged in default behavior. v0.14.6 adds opt-in RFC 9457 problem+json and onDeny response hooks to the access-refusal middleware (defaults unchanged, so nothing the shop relies on shifts) and corrects a few internal export and bearer-scope paths. No operator action is required. **Changed:** *Vendored blamejs runtime updated to v0.14.6* — The pinned blamejs runtime moves from v0.14.5 to v0.14.6. The middleware the storefront and admin compose keeps its current default behavior; the release adds opt-in problem-details / onDeny response hooks to the refusal middleware and fixes internal export and bearer-scope paths. No behavior change for the shop.
|
|
12
14
|
|
|
13
15
|
- v0.3.7 (2026-05-30) — **Shipping zones now apply at checkout.** The shipping zones you define in the admin console — per-region rates by destination, parcel weight, and order value — now drive the shipping options shown at checkout. When a cart's destination matches a zone, the shopper sees that zone's rates; when no zone matches, or none are defined, checkout falls back to the existing flat-rate table exactly as before, so a store that has not configured zones is entirely unaffected. A fully digital order is never quoted physical shipping. The admin screen for managing zones already shipped; this wires it through to the checkout quote. **Added:** *Zone-based shipping rates at checkout* — When a cart's destination matches a configured shipping zone, the checkout shipping quote uses that zone's rates — resolved by destination country and region, parcel weight, and order subtotal — instead of the flat-rate table. With no matching zone, or no zones defined, checkout returns the existing flat rates unchanged, so the change stays invisible until you set zones up; any lookup error degrades to the same flat fallback. A fully digital order (no shipping-requiring item) is never quoted physical shipping.
|
package/lib/asset-manifest.json
CHANGED
package/lib/checkout.js
CHANGED
|
@@ -146,6 +146,17 @@ function create(deps) {
|
|
|
146
146
|
// server-authoritative computation the cart page renders, so the
|
|
147
147
|
// "review your order" subtotal and the charged amount agree.
|
|
148
148
|
var quantityDiscounts = deps.quantityDiscounts || null;
|
|
149
|
+
// Optional automatic-discount engine. When wired, the quote consults
|
|
150
|
+
// every operator-authored auto-discount rule against the resolved
|
|
151
|
+
// cart and reduces the subtotal by the matched savings (clamped to
|
|
152
|
+
// [0, subtotal] so a rule can never drive the order negative or
|
|
153
|
+
// charge more than the cart is worth). Disabled when absent — the
|
|
154
|
+
// discount is zero and every total is identical to the un-wired
|
|
155
|
+
// flow, so production stays unchanged until a rule is defined. Only
|
|
156
|
+
// subtotal-reducing savings feed `discount_minor`; a `free_shipping`
|
|
157
|
+
// rule's savings are a shipping concern and are left to the shipping
|
|
158
|
+
// primitive, not folded into the subtotal discount here.
|
|
159
|
+
var autoDiscount = deps.autoDiscount || null;
|
|
149
160
|
|
|
150
161
|
// Reprice a list of cart lines through the quantity-discount engine.
|
|
151
162
|
// Returns a shallow copy with `unit_amount_minor` overwritten by the
|
|
@@ -179,6 +190,72 @@ function create(deps) {
|
|
|
179
190
|
return out;
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
// Resolve the automatic-discount effect for a priced cart. Builds
|
|
194
|
+
// the engine's cart shape from the resolved lines + subtotal, asks
|
|
195
|
+
// the engine which operator-authored rules apply, and returns the
|
|
196
|
+
// subtotal-reducing savings (clamped to [0, subtotal]) plus the
|
|
197
|
+
// per-rule breakdown the post-commit recorder replays.
|
|
198
|
+
//
|
|
199
|
+
// FALLBACK (zero regression): no engine wired, a non-finite or
|
|
200
|
+
// negative aggregate, an empty result, or ANY throw collapses to
|
|
201
|
+
// `{ discount_minor: 0, applied: [] }` — byte-identical to the
|
|
202
|
+
// un-wired flow. The engine is consulted but never trusted to keep
|
|
203
|
+
// the buy path alive.
|
|
204
|
+
//
|
|
205
|
+
// PAYMENT SAFETY: the returned discount is clamped to
|
|
206
|
+
// [0, subtotalMinor] so it can never be negative and never exceeds
|
|
207
|
+
// the subtotal — the order total can therefore never go negative,
|
|
208
|
+
// and a customer can never be charged less than zero or credited
|
|
209
|
+
// more than the cart is worth.
|
|
210
|
+
async function _resolveAutoDiscount(lines, subtotalMinor, customerId) {
|
|
211
|
+
if (!autoDiscount || typeof autoDiscount.evaluate !== "function") {
|
|
212
|
+
return { discount_minor: 0, applied: [] };
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
var evalLines = [];
|
|
216
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
217
|
+
var l = lines[i];
|
|
218
|
+
evalLines.push({
|
|
219
|
+
sku: l.sku,
|
|
220
|
+
quantity: l.qty,
|
|
221
|
+
unit_price_minor: l.unit_amount_minor,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
var res = await autoDiscount.evaluate({
|
|
225
|
+
cart: {
|
|
226
|
+
subtotal_minor: subtotalMinor,
|
|
227
|
+
lines: evalLines,
|
|
228
|
+
},
|
|
229
|
+
customer_id: customerId || undefined,
|
|
230
|
+
});
|
|
231
|
+
var appliedRaw = res && Array.isArray(res.applied) ? res.applied : [];
|
|
232
|
+
var total = 0;
|
|
233
|
+
var applied = [];
|
|
234
|
+
for (var j = 0; j < appliedRaw.length; j += 1) {
|
|
235
|
+
var entry = appliedRaw[j];
|
|
236
|
+
// A `free_shipping` rule's savings are a shipping reduction,
|
|
237
|
+
// not a subtotal discount — excluded so the subtotal-level
|
|
238
|
+
// `discount_minor` stays a pure subtotal reduction (the
|
|
239
|
+
// shipping primitive owns the shipping side).
|
|
240
|
+
if (!entry || entry.value_kind === "free_shipping") continue;
|
|
241
|
+
var s = entry.savings_minor;
|
|
242
|
+
if (!Number.isInteger(s) || s <= 0) continue;
|
|
243
|
+
total += s;
|
|
244
|
+
applied.push({ rule_slug: entry.rule_slug, savings_minor: s });
|
|
245
|
+
}
|
|
246
|
+
if (!Number.isFinite(total) || total <= 0) {
|
|
247
|
+
return { discount_minor: 0, applied: [] };
|
|
248
|
+
}
|
|
249
|
+
// Clamp to the subtotal floor: the discount can reduce the
|
|
250
|
+
// subtotal to zero but never past it.
|
|
251
|
+
if (total > subtotalMinor) total = subtotalMinor;
|
|
252
|
+
return { discount_minor: total, applied: applied };
|
|
253
|
+
} catch (_e) {
|
|
254
|
+
// Any engine failure -> no discount, never a 5xx on the buy path.
|
|
255
|
+
return { discount_minor: 0, applied: [] };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
182
259
|
// Validate a gift-card code against a priced quote: the card exists,
|
|
183
260
|
// is active, not expired, and matches the order currency. Returns
|
|
184
261
|
// the credit to apply (capped at the grand total so an order total
|
|
@@ -336,6 +413,33 @@ function create(deps) {
|
|
|
336
413
|
});
|
|
337
414
|
}
|
|
338
415
|
|
|
416
|
+
// Record each applied auto-discount rule against a committed order
|
|
417
|
+
// so the engine's redemption counter + event log reflect the use.
|
|
418
|
+
// Best-effort / drop-silent: the customer has already been charged
|
|
419
|
+
// (or the order is otherwise placed) by the time this runs, so a
|
|
420
|
+
// recording failure must NEVER fail or reverse the order. Each
|
|
421
|
+
// record is isolated in its own try/catch so one bad row can't stop
|
|
422
|
+
// the rest. No-op when the engine isn't wired or nothing applied.
|
|
423
|
+
async function _recordAutoDiscounts(quote, orderId, customerId) {
|
|
424
|
+
if (!autoDiscount || typeof autoDiscount.recordApplication !== "function") return;
|
|
425
|
+
var applied = quote && Array.isArray(quote.auto_discounts) ? quote.auto_discounts : [];
|
|
426
|
+
for (var i = 0; i < applied.length; i += 1) {
|
|
427
|
+
var a = applied[i];
|
|
428
|
+
if (!a || !a.rule_slug) continue;
|
|
429
|
+
try {
|
|
430
|
+
await autoDiscount.recordApplication({
|
|
431
|
+
rule_slug: a.rule_slug,
|
|
432
|
+
order_id: orderId,
|
|
433
|
+
savings_minor: a.savings_minor,
|
|
434
|
+
customer_id: customerId || undefined,
|
|
435
|
+
});
|
|
436
|
+
} catch (_e) {
|
|
437
|
+
// drop-silent — by design: the order is already placed; a
|
|
438
|
+
// failed redemption record must not surface to the buyer.
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
339
443
|
// Compose a quote from a cart + ship-to + (optional) selected
|
|
340
444
|
// shipping service. Pure read — no DB writes.
|
|
341
445
|
async function _buildQuote(input) {
|
|
@@ -390,10 +494,18 @@ function create(deps) {
|
|
|
390
494
|
}
|
|
391
495
|
|
|
392
496
|
var shippingMinor = selected ? selected.amount_minor : 0;
|
|
497
|
+
|
|
498
|
+
// Automatic discounts: consult the operator-authored rule engine
|
|
499
|
+
// (when wired) for the subtotal reduction this cart earns. The
|
|
500
|
+
// resolver clamps the result to [0, subtotal] and falls back to 0
|
|
501
|
+
// on any failure, so the total math below is identical to the
|
|
502
|
+
// un-wired flow whenever no rule applies.
|
|
503
|
+
var autoDisc = await _resolveAutoDiscount(lines, sub.amount_minor, c.customer_id || null);
|
|
504
|
+
|
|
393
505
|
var totals = pricing.totals(c, lines, {
|
|
394
506
|
tax_minor: taxRow.tax_minor,
|
|
395
507
|
shipping_minor: shippingMinor,
|
|
396
|
-
discount_minor:
|
|
508
|
+
discount_minor: autoDisc.discount_minor,
|
|
397
509
|
});
|
|
398
510
|
|
|
399
511
|
return {
|
|
@@ -405,6 +517,10 @@ function create(deps) {
|
|
|
405
517
|
shipping_rates: ratesRow.services,
|
|
406
518
|
selected_shipping: selected,
|
|
407
519
|
totals: totals,
|
|
520
|
+
// Per-rule breakdown that fed `totals.discount_minor`, replayed
|
|
521
|
+
// by confirm()'s post-commit recorder. Empty when no rule
|
|
522
|
+
// applied (or no engine wired).
|
|
523
|
+
auto_discounts: autoDisc.applied,
|
|
408
524
|
};
|
|
409
525
|
}
|
|
410
526
|
|
|
@@ -499,6 +615,10 @@ function create(deps) {
|
|
|
499
615
|
});
|
|
500
616
|
if (gc) await _redeemGiftCard(gc, paidOrder.id);
|
|
501
617
|
if (loy) await _redeemLoyalty(loy, loyaltyCustomerId, paidOrder.id);
|
|
618
|
+
// Best-effort: record the auto-discount redemptions against the
|
|
619
|
+
// placed order. Runs post-commit so a recording failure can't
|
|
620
|
+
// reverse an order that's already paid.
|
|
621
|
+
await _recordAutoDiscounts(quote, paidOrder.id, cartRow.customer_id || null);
|
|
502
622
|
await cart.setStatus(quote.cart_id, "converted");
|
|
503
623
|
var settled = await order.transition(paidOrder.id, "mark_paid", {
|
|
504
624
|
reason: loy && !gc ? "loyalty:full" : "gift_card:full",
|
|
@@ -552,6 +672,10 @@ function create(deps) {
|
|
|
552
672
|
// guard.
|
|
553
673
|
if (gc) await _redeemGiftCard(gc, createdOrder.id);
|
|
554
674
|
if (loy) await _redeemLoyalty(loy, loyaltyCustomerId, createdOrder.id);
|
|
675
|
+
// Best-effort: record the auto-discount redemptions against the
|
|
676
|
+
// placed order. Runs post-commit so a recording failure can't
|
|
677
|
+
// reverse or fail an order whose PaymentIntent already exists.
|
|
678
|
+
await _recordAutoDiscounts(quote, createdOrder.id, cartRow.customer_id || null);
|
|
555
679
|
|
|
556
680
|
// Mark the cart converted so a refresh of the storefront
|
|
557
681
|
// doesn't accidentally re-quote the same cart.
|
package/lib/storefront.js
CHANGED
|
@@ -7139,7 +7139,7 @@ function mount(router, deps) {
|
|
|
7139
7139
|
|
|
7140
7140
|
// Compute the cart/checkout totals the shopper sees BEFORE paying.
|
|
7141
7141
|
// Composes the SAME tax + shipping primitives the charge runs through
|
|
7142
|
-
// (via checkout.quote, which prices tax against the
|
|
7142
|
+
// (via checkout.quote, which prices tax against the pre-discount
|
|
7143
7143
|
// subtotal + picks shipping rates for the destination), so the
|
|
7144
7144
|
// displayed grand total agrees with what Stripe is later asked to
|
|
7145
7145
|
// charge for that destination. Returns:
|
package/package.json
CHANGED