@blamejs/blamejs-shop 0.4.99 → 0.4.101
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 +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/cart-bulk-ops.js +7 -1
- package/lib/checkout.js +46 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.101 (2026-06-25) — **Bulk cart operations now use the same per-line quantity ceiling as a normal add-to-cart, so a large line is no longer un-bulk-addable or silently reduced.** The cart enforces a per-line quantity ceiling on every add-to-cart. The bulk-operations surface (bulk add and reorder) carried its own copy of that ceiling that had drifted ten times lower than the cart's. The mismatch meant a line legitimately built up to the cart's ceiling through normal add-to-cart could not be added to in bulk (the whole bulk batch was rejected), and a reorder of such a line silently clamped its quantity down instead of preserving it. Bulk operations now source the ceiling directly from the cart primitive, so the two always agree: the same quantity that add-to-cart accepts is accepted in bulk, and a reorder no longer quietly reduces a line. No configuration changes. **Fixed:** *Bulk add and reorder share the cart's per-line quantity ceiling* — Bulk cart operations validated and clamped line quantities against their own ceiling, which was ten times lower than the one the cart applies on a normal add-to-cart. As a result a line whose quantity was legitimately built up to the cart's ceiling could not be bulk-added to — the entire batch was refused — and a reorder of that line silently clamped its quantity down rather than keeping it. The bulk surface now reads the ceiling from the cart primitive itself, so bulk add accepts exactly what add-to-cart accepts and a reorder preserves a line's quantity instead of reducing it.
|
|
12
|
+
|
|
13
|
+
- v0.4.100 (2026-06-25) — **A coupon-stacking policy is now enforced at checkout, so two code-gated discounts can no longer combine past a policy that forbids it.** The coupon-stacking policy surface decides which discount codes may combine on one order — whether codes stack at all, a per-order code cap, and codes that are exclusive of everything else. That policy was applied in the admin surface but never on the buy path: checkout summed every code-gated rule a shopper had unlocked, so an operator who authored a policy forbidding combination still had it ignored on the charged price, and a customer could stack two codes the policy said could not combine. Checkout now consults the stacking policy before the discount engine sums anything: when an active policy governs the cart, the presented codes are narrowed to the policy-approved subset (honoring the non-combinable, over-cap, and exclusive-code rules) and only that subset unlocks discounts. Governance is opt-in — with no active policy the presented codes pass through exactly as before, so carts that never used stacking policies are unaffected. Operators who offer multiple combinable codes and want them to keep stacking should define a policy with allow_combine.with_other_codes enabled. **Fixed:** *Code-gated discounts now honor the coupon-stacking policy at checkout* — Checkout resolved automatic discounts by summing every rule a shopper's presented codes unlocked, with no consultation of the coupon-stacking policy — so a policy that forbade combining codes, capped the number of codes per order, or marked a code exclusive was enforced only in the admin surface, never on the price the customer was actually charged. Checkout now evaluates the stacking policy first: when an active policy governs the cart, the presented codes are reduced to the policy-approved subset before any discount is computed, so non-combinable, over-cap, and exclusive-code rules hold on the charged total. When no stacking policy is active the codes are left untouched, so existing carts that don't use stacking policies see no change; to keep multiple codes combining, define a policy with allow_combine.with_other_codes enabled.
|
|
14
|
+
|
|
11
15
|
- v0.4.99 (2026-06-25) — **A subscription whose dunning retries are exhausted is now actually cancelled, instead of being marked cancelled while it keeps billing.** Dunning walks a failed-payment enrollment through its retry schedule and, when the attempt cap is reached, is documented to auto-cancel the subscription. Only an explicit cancel_subscription step in the schedule actually composed the subscription-side cancel, though: when the cap fired on the final retry/reminder step instead — the common case for a schedule that simply runs out of attempts — the enrollment was flipped to 'cancelled' but the subscription cancel was never propagated, so the recurring charge kept firing against a customer the policy had given up on. The attempt-cap close now composes the same subscription cancel an explicit cancel step does, and records it as its own audit event with its real outcome (a failed cancel is visible, not masked). A separate audit-integrity fix removes a hardcoded 'cancelled the subscription' event that the schedule-exhaustion branch wrote unconditionally, including when no cancel was attempted. No configuration changes; operators running dunning policies that rely on the attempt cap (rather than an explicit cancel step) should upgrade. **Fixed:** *Reaching the dunning attempt cap now cancels the subscription, not just the enrollment row* — When a dunning enrollment reached cancel_after_attempts on a retry or reminder step, the enrollment was closed as 'cancelled' but the subscription-side cancellation was composed only for an explicit cancel_subscription step — so a policy that relied on the attempt cap to give up left the underlying subscription active and continuing to bill. The attempt-cap close now composes the subscription cancel that the documented 'tried everything' exit always promised, resolving the invoice's subscription and signaling its cancellation exactly as an explicit cancel step does. The cancel is recorded as its own entry in the dunning audit log with the actual outcome, so a cancel that fails downstream is surfaced for follow-up rather than reported as success. · *The dunning audit log no longer records a cancellation that did not happen* — The schedule-exhaustion path wrote a fixed 'cancel_subscription / ok' audit entry regardless of how the enrollment actually closed — so an enrollment that ended without a cancellation still showed a successful cancel in the history an operator replays on the customer's timeline. The terminal audit entry now reflects what actually happened: a real cancel with its real outcome when the subscription is cancelled, and a plain schedule-exhausted record when it is not.
|
|
12
16
|
|
|
13
17
|
- v0.4.98 (2026-06-25) — **A concurrent vendor registration with the same slug now returns a typed duplicate error instead of leaking a raw database error.** Registering a vendor checks whether the slug is already taken before inserting, then inserts. Two registrations of the same slug racing each other can both pass that pre-check, so the second insert collides on the slug primary key. That insert had no failure handling, so the raw database error surfaced — and in production, where the data layer redacts it to a generic 'HTTP 500', the caller got an opaque error rather than the typed 'slug already registered' it gets from the pre-check. The insert is now wrapped: on any failure it re-reads by slug, and if a row exists it raises the same typed duplicate error the pre-check raises, otherwise it re-throws. No configuration changes. **Fixed:** *Racing vendor registrations surface a typed duplicate error, not a raw 500* — vendors.registerVendor pre-checks the slug and then inserts, but a concurrent registration of the same slug can slip past the pre-check and collide on the slug primary key. The insert is now wrapped to detect that state-agnostically — re-reading by slug after a failure and raising the same typed VENDOR_SLUG_TAKEN error the pre-check raises (rather than leaking the raw constraint error, which production redacts to a bare 500). Mirrors the assignSku duplicate handling.
|
package/lib/asset-manifest.json
CHANGED
package/lib/cart-bulk-ops.js
CHANGED
|
@@ -66,7 +66,13 @@ var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,198}[a-z0-9])?$/;
|
|
|
66
66
|
var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
67
67
|
var CURRENCY_RE = /^[A-Z]{3}$/;
|
|
68
68
|
var MAX_BULK = 500;
|
|
69
|
-
|
|
69
|
+
// The per-line quantity ceiling is owned by the cart primitive — source it
|
|
70
|
+
// from there so bulk operations and cart.addLine never diverge on the max a
|
|
71
|
+
// single line may hold. A hand-copied value had drifted 10x lower, which made
|
|
72
|
+
// a line legitimately built past it via cart.addLine un-bulk-addable (addLines
|
|
73
|
+
// threw) and silently clamped on reorder. (cart.js does not require this
|
|
74
|
+
// module, so the top-level require is acyclic.)
|
|
75
|
+
var MAX_QTY = require("./cart").MAX_QTY;
|
|
70
76
|
var SPLIT_BY = Object.freeze(["vendor", "category", "drop_ship_supplier"]);
|
|
71
77
|
|
|
72
78
|
// ---- validators ---------------------------------------------------------
|
package/lib/checkout.js
CHANGED
|
@@ -228,6 +228,13 @@ function create(deps) {
|
|
|
228
228
|
// rule's savings are a shipping concern and are left to the shipping
|
|
229
229
|
// primitive, not folded into the subtotal discount here.
|
|
230
230
|
var autoDiscount = deps.autoDiscount || null;
|
|
231
|
+
// Optional coupon-stacking governor. When wired, it decides which of
|
|
232
|
+
// the shopper-presented codes may combine on one order (the operator's
|
|
233
|
+
// allow_combine / max_codes_per_order / exclusive_codes policy) before
|
|
234
|
+
// the discount engine sums anything — so two code-gated rules can't
|
|
235
|
+
// silently stack past a policy that forbids it. Absent → every
|
|
236
|
+
// presented code reaches the engine (the pre-governor behaviour).
|
|
237
|
+
var couponStacking = deps.couponStacking || null;
|
|
231
238
|
// Optional discount-allocation recorder. When wired, a placed order
|
|
232
239
|
// that carried a cart-level discount gets a per-line breakdown of how
|
|
233
240
|
// that discount split across the order lines, recorded as a back-office
|
|
@@ -550,6 +557,42 @@ function create(deps) {
|
|
|
550
557
|
unit_price_minor: l.unit_amount_minor,
|
|
551
558
|
});
|
|
552
559
|
}
|
|
560
|
+
// Apply the coupon-stacking policy BEFORE the engine sums anything.
|
|
561
|
+
// Only the surviving subset of presented codes unlocks code-gated
|
|
562
|
+
// rules, so a non-stackable / exclusive / over-cap code cannot
|
|
563
|
+
// inflate the discount. Only consulted when the governor is wired
|
|
564
|
+
// AND more than one code is presented (a single code can never
|
|
565
|
+
// "stack"); drop-silent on any consult failure so a governor outage
|
|
566
|
+
// never breaks the buy path — the discount clamp below still bounds
|
|
567
|
+
// the result to [0, subtotal].
|
|
568
|
+
var effectiveCodes = Array.isArray(codes) && codes.length ? codes : undefined;
|
|
569
|
+
if (couponStacking && typeof couponStacking.evaluate === "function" &&
|
|
570
|
+
typeof couponStacking.activePoliciesForCart === "function" &&
|
|
571
|
+
Array.isArray(codes) && codes.length > 1) {
|
|
572
|
+
try {
|
|
573
|
+
// Governance is opt-in: only an ACTIVE stacking policy narrows the
|
|
574
|
+
// presented codes. With no governing policy the operator hasn't
|
|
575
|
+
// asked checkout to police stacking, so the codes pass through
|
|
576
|
+
// unchanged — the cart accepted them and silently dropping one
|
|
577
|
+
// here would diverge the charged price from what the cart showed.
|
|
578
|
+
var govCart = { subtotal_minor: subtotalMinor };
|
|
579
|
+
var policies = await couponStacking.activePoliciesForCart({
|
|
580
|
+
cart: govCart, customer_id: customerId || undefined,
|
|
581
|
+
});
|
|
582
|
+
if (Array.isArray(policies) && policies.length > 0) {
|
|
583
|
+
var stack = await couponStacking.evaluate({
|
|
584
|
+
codes: codes,
|
|
585
|
+
cart: govCart,
|
|
586
|
+
customer_id: customerId || undefined,
|
|
587
|
+
});
|
|
588
|
+
if (stack && Array.isArray(stack.applied)) {
|
|
589
|
+
effectiveCodes = stack.applied.length ? stack.applied : undefined;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
} catch (_e) {
|
|
593
|
+
effectiveCodes = Array.isArray(codes) && codes.length ? codes : undefined;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
553
596
|
var res = await autoDiscount.evaluate({
|
|
554
597
|
cart: {
|
|
555
598
|
subtotal_minor: subtotalMinor,
|
|
@@ -558,8 +601,9 @@ function create(deps) {
|
|
|
558
601
|
customer_id: customerId || undefined,
|
|
559
602
|
// Shopper-presented coupon codes unlock code-gated rules. Absent /
|
|
560
603
|
// empty leaves only the pure-automatic rules in play, so a quote
|
|
561
|
-
// with no codes is byte-identical to the pre-code behaviour.
|
|
562
|
-
|
|
604
|
+
// with no codes is byte-identical to the pre-code behaviour. The
|
|
605
|
+
// coupon-stacking governor (above) may narrow this set.
|
|
606
|
+
codes: effectiveCodes,
|
|
563
607
|
});
|
|
564
608
|
var appliedRaw = res && Array.isArray(res.applied) ? res.applied : [];
|
|
565
609
|
var total = 0;
|
package/package.json
CHANGED