@blamejs/blamejs-shop 0.4.102 → 0.4.104
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/admin.js +50 -0
- package/lib/affiliates.js +2 -2
- package/lib/api-keys.js +1 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/auto-discount.js +4 -4
- package/lib/auto-replenish.js +1 -1
- package/lib/backorder.js +2 -2
- package/lib/carrier-accounts.js +1 -1
- package/lib/cart-bulk-ops.js +1 -1
- package/lib/cart.js +2 -2
- package/lib/catalog.js +3 -3
- package/lib/click-and-collect.js +2 -2
- package/lib/customer-merge.js +2 -2
- package/lib/customer-surveys.js +1 -1
- package/lib/cycle-counting.js +1 -1
- package/lib/dropship-forwarding.js +1 -1
- package/lib/dunning.js +1 -1
- package/lib/email-campaigns.js +4 -4
- package/lib/giftcards.js +6 -6
- package/lib/inventory-allocations.js +1 -1
- package/lib/inventory-audits.js +1 -1
- package/lib/inventory-receive.js +2 -2
- package/lib/inventory-writeoffs.js +1 -1
- package/lib/invoice-renderer.js +1 -1
- package/lib/knowledge-base.js +1 -1
- package/lib/loyalty-redemption.js +3 -3
- package/lib/loyalty.js +5 -5
- package/lib/operator-approvals.js +1 -1
- package/lib/operator-help-center.js +1 -1
- package/lib/order-exchanges.js +1 -1
- package/lib/order-export.js +4 -4
- package/lib/payment-retries.js +2 -2
- package/lib/pick-lists.js +1 -1
- package/lib/plan-changes.js +1 -1
- package/lib/preorder.js +4 -4
- package/lib/print-on-demand.js +1 -1
- package/lib/print-queue.js +2 -2
- package/lib/purchase-orders.js +1 -1
- package/lib/push-notifications.js +2 -2
- package/lib/quotes.js +7 -7
- package/lib/referrals.js +3 -3
- package/lib/reorder-reminders.js +1 -1
- package/lib/returns.js +4 -4
- package/lib/seller-signup.js +1 -1
- package/lib/shipping-insurance.js +2 -2
- package/lib/sms-dispatcher.js +2 -2
- package/lib/split-shipments.js +1 -1
- package/lib/stock-alerts.js +1 -1
- package/lib/stock-transfers.js +4 -4
- package/lib/subscription-gifts.js +2 -2
- package/lib/suggestion-box.js +2 -2
- package/lib/webhooks.js +10 -3
- package/lib/wishlist-alerts.js +1 -1
- package/lib/wishlist-digest.js +1 -1
- 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.104 (2026-06-25) — **Compare-and-swap atomic claims across the framework now decide won/lost through the shared row-count primitive instead of hand-rolled checks.** Throughout the framework, an atomic claim on a single-statement-per-request backend is a guarded write — UPDATE ... WHERE the row is still in its expected state, or a conditional INSERT — whose affected-row count decides whether this caller won the race (exactly one row) or lost and must no-op. That won/lost decision was hand-rolled at each site as a Number(result.rowCount || 0) === 1 check, which silently treats a result carrying no recognizable row-count as a lost race. Every such site (104 across commissions, redemption caps, inventory and stock-transfer claims, fulfillment and replenishment leases, gift-card and loyalty ledger writes, webhook delivery leases, and more) now routes that decision through the framework's shared row-count primitive, which owns the exactly-one-row check and the cross-adapter row-count field naming, and surfaces an indeterminate result instead of silently counting it as lost. On the production database path the affected-row count is always reported, so there is no behavior change; the change centralizes the interpretation and makes an otherwise-silent indeterminate result observable. No configuration changes and no schema changes. **Changed:** *Atomic compare-and-swap claims use the shared row-count primitive for their won/lost decision* — Compare-and-swap atomic claims — the guarded UPDATE / conditional INSERT pattern used wherever two requests can race the same row on a backend without interactive transactions — previously decided won-versus-lost with a hand-written row-count comparison at each call site, and a result that carried no recognizable row-count was silently taken as a lost race. These claims now make that decision through the framework's shared row-count primitive, which centralizes the exactly-one-row check and the adapter-specific row-count field naming and raises an error on an indeterminate result rather than treating it as a loss. Because the production database always reports the affected-row count, the won/lost outcome is unchanged on every real path; the benefit is one consistent, audited interpretation and a previously-silent indeterminate result now surfacing. A hand-rolled cross-adapter row-count extraction in the webhook delivery-retry lease is removed in favor of the same primitive.
|
|
12
|
+
|
|
13
|
+
- v0.4.103 (2026-06-25) — **A return refund that exceeds the order's remaining refundable balance is now refused cleanly, instead of failing at the payment provider.** The direct refund console refuses an over-refund up front: it compares the requested amount against the order's remaining refundable balance and returns a clear 'more than is still refundable' error before any money moves. The provider-backed return (RMA) refund path skipped that check — it issued the RMA's approved amount straight to the payment provider, relying on the provider to reject anything past the captured charge. The result was a late, opaque provider-failure error (and a returns workflow left mid-flight) instead of the clean, actionable refusal the console gives. The RMA refund path now applies the same remaining-balance guard before calling the provider: an approved amount greater than what remains refundable — including a return stacked on top of an earlier partial refund — is refused with the same clear message, the return is left in its received state so the operator can re-approve a corrected amount, and the payment provider is never dialed. A refund of an already-refunded return continues to return the existing conflict response. No configuration changes. **Fixed:** *Return (RMA) refunds honor the order's remaining-balance cap before reaching the payment provider* — A provider-backed return refund issued the return's approved amount directly to Stripe or PayPal with no remaining-balance check, so an amount larger than what was still refundable on the order — for example a return approved for more than the order total, or stacked on top of an earlier partial refund — failed only at the payment provider, surfacing as an opaque provider-failure error rather than the clean refusal the direct refund console returns. The return refund path now compares the approved amount against the order's remaining refundable balance first: an over-cap amount is refused with a clear message, the return stays in its received state so a corrected amount can be re-approved, and the provider is never called. A refund attempt on an already-refunded return still returns the existing conflict response.
|
|
14
|
+
|
|
11
15
|
- v0.4.102 (2026-06-25) — **Vendored framework refreshed to 0.15.26.** Updates the vendored blamejs framework to 0.15.26. The substantive upstream change is a worker-thread handle leak fix in the sandbox primitive: b.sandbox.run now awaits the worker's termination before resolving, so repeatedly running sandboxed code in a long-lived process no longer accumulates leaked MessagePort handles that keep the event loop alive and slow graceful shutdown. The follow-up 0.15.26 build is upstream test-harness correctness only and ships a library byte-for-byte identical to 0.15.25. No runtime API changes and no behavior change for this storefront, which does not run untrusted code through the sandbox primitive; no operator action required. **Changed:** *Vendored framework refreshed to 0.15.26* — Refreshes the vendored blamejs framework to 0.15.26. Upstream 0.15.25 fixes a worker-thread MessagePort leak in the sandbox primitive (b.sandbox.run now waits for the worker to terminate before resolving), and 0.15.26 is a test-harness-only build whose shipped library is identical to 0.15.25. There are no runtime API changes; this storefront does not expose the sandbox primitive on any request path, so there is no operator-visible behavior change and no action required.
|
|
12
16
|
|
|
13
17
|
- 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.
|
package/lib/admin.js
CHANGED
|
@@ -4784,6 +4784,12 @@ function mount(router, deps) {
|
|
|
4784
4784
|
if (!e) return null;
|
|
4785
4785
|
if (e.code === "RMA_NOT_FOUND") return { status: 404, slug: "return-not-found" };
|
|
4786
4786
|
if (e.code === "RMA_TRANSITION_REFUSED") return { status: 409, slug: "return-transition-refused" };
|
|
4787
|
+
// A refund-cap refusal raised by the provider-refund pre-check (the same
|
|
4788
|
+
// over-refund / nothing-remaining guards the direct console maps to 422)
|
|
4789
|
+
// is the operator's problem — a clean 422, not a 502 provider failure.
|
|
4790
|
+
if (e._refundCode === "over-refund" || e._refundCode === "nothing-remaining") {
|
|
4791
|
+
return { status: 422, slug: e._refundCode };
|
|
4792
|
+
}
|
|
4787
4793
|
return null;
|
|
4788
4794
|
}
|
|
4789
4795
|
|
|
@@ -5173,11 +5179,55 @@ function mount(router, deps) {
|
|
|
5173
5179
|
// stamped at claim time is the operator's own text when supplied, else
|
|
5174
5180
|
// a stable marker; the live provider refund id is on the audit row +
|
|
5175
5181
|
// the JSON response, so it stays reconcilable without a second write.
|
|
5182
|
+
// Claiming first also keeps an already-refunded RMA a clean 409
|
|
5183
|
+
// (the atomic claim refuses it) rather than masking it behind the
|
|
5184
|
+
// amount guard below.
|
|
5176
5185
|
var claimed = await returns.refund(rma.id, {
|
|
5177
5186
|
operator_notes: (body.operator_notes && body.operator_notes.length)
|
|
5178
5187
|
? body.operator_notes
|
|
5179
5188
|
: "provider refund issued",
|
|
5180
5189
|
});
|
|
5190
|
+
|
|
5191
|
+
// Over-refund guard — claim succeeded, but the approved amount must
|
|
5192
|
+
// still fit the order's remaining refundable balance. Mirror the
|
|
5193
|
+
// direct console's over-refund cap BEFORE the provider is dialed, so
|
|
5194
|
+
// an RMA refund_amount_minor that exceeds what remains (already fully
|
|
5195
|
+
// refunded, or stacked past the order total via a prior console/RMA
|
|
5196
|
+
// refund) is refused cleanly (422) instead of failing late at the PSP
|
|
5197
|
+
// as a 502. recordPartialRefund (below) folds each RMA refund into
|
|
5198
|
+
// refundedTotalMinor, so `remaining` reflects prior console AND RMA
|
|
5199
|
+
// refunds alike. On refusal the just-taken claim is RELEASED (back to
|
|
5200
|
+
// received, retryable with a corrected amount) so the RMA never
|
|
5201
|
+
// strands in `refunded` with no money moved. An unset
|
|
5202
|
+
// refund_amount_minor means "refund what remains" — the provider caps
|
|
5203
|
+
// it at the capturable balance, so it proceeds unless nothing remains.
|
|
5204
|
+
// The whole guard runs inside a try/catch that RELEASES the claim on
|
|
5205
|
+
// ANY throw — whether the deliberate over-cap refusal below OR a
|
|
5206
|
+
// transient failure of the balance read itself. Without that, a
|
|
5207
|
+
// refundedTotalMinor read that rejects after the claim moved the RMA to
|
|
5208
|
+
// `refunded` would exit before the provider-call release path, stranding
|
|
5209
|
+
// the RMA behind a 409 with no money moved.
|
|
5210
|
+
try {
|
|
5211
|
+
var rmaAlreadyMinor = await order.refundedTotalMinor(order2.id);
|
|
5212
|
+
var rmaRemainingMinor = (Number(order2.grand_total_minor) || 0) - rmaAlreadyMinor;
|
|
5213
|
+
if (rmaRemainingMinor <= 0 ||
|
|
5214
|
+
(rma.refund_amount_minor != null && rma.refund_amount_minor > rmaRemainingMinor)) {
|
|
5215
|
+
var capErr = (rmaRemainingMinor <= 0)
|
|
5216
|
+
? new TypeError("This order is already fully refunded — nothing remains to refund.")
|
|
5217
|
+
: new TypeError("That RMA refund of " + pricing.format(rma.refund_amount_minor, order2.currency) +
|
|
5218
|
+
" is more than the " + pricing.format(rmaRemainingMinor, order2.currency) +
|
|
5219
|
+
" still refundable on this order.");
|
|
5220
|
+
capErr._refundCode = (rmaRemainingMinor <= 0) ? "nothing-remaining" : "over-refund";
|
|
5221
|
+
throw capErr;
|
|
5222
|
+
}
|
|
5223
|
+
} catch (guardErr) {
|
|
5224
|
+
// Release the just-taken claim (back to received, retryable) so
|
|
5225
|
+
// neither the over-cap refusal nor a transient read failure leaves
|
|
5226
|
+
// the RMA stranded in `refunded`. No money has moved yet.
|
|
5227
|
+
try { await returns.releaseRefundClaim(rma.id); }
|
|
5228
|
+
catch (_relErr) { /* drop-silent — guardErr below is the actionable one */ }
|
|
5229
|
+
throw guardErr;
|
|
5230
|
+
}
|
|
5181
5231
|
var idem = "rma-refund:" + rma.id;
|
|
5182
5232
|
var refund;
|
|
5183
5233
|
var providerRefund;
|
package/lib/affiliates.js
CHANGED
|
@@ -925,7 +925,7 @@ function create(opts) {
|
|
|
925
925
|
"WHERE id = ?3 AND status = 'pending'",
|
|
926
926
|
[paidAt, reference, id],
|
|
927
927
|
);
|
|
928
|
-
if (
|
|
928
|
+
if (!b.sql.casWon(paidUpd).won) {
|
|
929
929
|
var racedPaid = await _getCommissionRaw(id);
|
|
930
930
|
var refusedPaid = new Error(
|
|
931
931
|
"affiliates.markCommissionPaid: refused — commission is " +
|
|
@@ -979,7 +979,7 @@ function create(opts) {
|
|
|
979
979
|
"WHERE id = ?3 AND status = 'pending'",
|
|
980
980
|
[ts, reason, id],
|
|
981
981
|
);
|
|
982
|
-
if (
|
|
982
|
+
if (!b.sql.casWon(voidUpd).won) {
|
|
983
983
|
var racedVoid = await _getCommissionRaw(id);
|
|
984
984
|
var refusedVoid = new Error(
|
|
985
985
|
"affiliates.markCommissionVoided: refused — commission is " +
|
package/lib/api-keys.js
CHANGED
|
@@ -580,7 +580,7 @@ function create(opts) {
|
|
|
580
580
|
"token_hash_previous = ?2, token_hash = ?3 WHERE id = ?4 AND status = 'active'",
|
|
581
581
|
[ts, current.token_hash, placeholderHash, id],
|
|
582
582
|
);
|
|
583
|
-
if (
|
|
583
|
+
if (!b.sql.casWon(rotateClaim).won) {
|
|
584
584
|
var lost = await _getRaw(id);
|
|
585
585
|
var refusedRotate = new Error(
|
|
586
586
|
"apiKeys.rotate: refused — key is " + (lost ? lost.status : "gone")
|
package/lib/asset-manifest.json
CHANGED
package/lib/auto-discount.js
CHANGED
|
@@ -1170,7 +1170,7 @@ function create(opts) {
|
|
|
1170
1170
|
"ON CONFLICT(rule_slug, order_id) DO NOTHING",
|
|
1171
1171
|
[id, slug, orderId, customerId, savings, appliedAt],
|
|
1172
1172
|
);
|
|
1173
|
-
if (
|
|
1173
|
+
if (b.sql.casWon(ins).won) {
|
|
1174
1174
|
await query(
|
|
1175
1175
|
"UPDATE auto_discount_rules SET redemptions_used = redemptions_used + 1, updated_at = ?1 WHERE slug = ?2",
|
|
1176
1176
|
[_now(), slug],
|
|
@@ -1252,7 +1252,7 @@ function create(opts) {
|
|
|
1252
1252
|
"WHERE slug = ?2 AND (max_redemptions_total IS NULL OR redemptions_used < max_redemptions_total)",
|
|
1253
1253
|
[ts, slug],
|
|
1254
1254
|
);
|
|
1255
|
-
if (
|
|
1255
|
+
if (!b.sql.casWon(counter).won) {
|
|
1256
1256
|
return { claimed: false, reason: "total-cap" };
|
|
1257
1257
|
}
|
|
1258
1258
|
|
|
@@ -1297,7 +1297,7 @@ function create(opts) {
|
|
|
1297
1297
|
}
|
|
1298
1298
|
throw e;
|
|
1299
1299
|
}
|
|
1300
|
-
if (
|
|
1300
|
+
if (!b.sql.casWon(ins).won) {
|
|
1301
1301
|
// Per-customer cap refused — return the total-cap reservation.
|
|
1302
1302
|
await query(
|
|
1303
1303
|
"UPDATE auto_discount_rules SET redemptions_used = " +
|
|
@@ -1326,7 +1326,7 @@ function create(opts) {
|
|
|
1326
1326
|
"UPDATE auto_discount_applications SET order_id = ?1 WHERE rule_slug = ?2 AND order_id = ?3",
|
|
1327
1327
|
[orderId, slug, claimRef],
|
|
1328
1328
|
);
|
|
1329
|
-
return
|
|
1329
|
+
return b.sql.casWon(res).won;
|
|
1330
1330
|
}
|
|
1331
1331
|
|
|
1332
1332
|
async function releaseClaim(input) {
|
package/lib/auto-replenish.js
CHANGED
|
@@ -770,7 +770,7 @@ function create(opts) {
|
|
|
770
770
|
"AND (last_run_at IS NULL OR last_run_at <= ?3)",
|
|
771
771
|
[now, policy.slug, now - interval],
|
|
772
772
|
);
|
|
773
|
-
if (
|
|
773
|
+
if (!b.sql.casWon(claim).won) continue; // lost the claim / not yet due
|
|
774
774
|
var runRow = await _runPolicyOnce(policy, now);
|
|
775
775
|
summaries.push(_shapeRun(runRow));
|
|
776
776
|
}
|
package/lib/backorder.js
CHANGED
|
@@ -332,7 +332,7 @@ function create(opts) {
|
|
|
332
332
|
"WHERE id = ?2 AND status = 'pending'",
|
|
333
333
|
[ts, line.id],
|
|
334
334
|
);
|
|
335
|
-
if (
|
|
335
|
+
if (!b.sql.casWon(fulfillClaim).won) {
|
|
336
336
|
throw new TypeError("backorder.fulfillBackorder: line is no longer pending " +
|
|
337
337
|
"(concurrently fulfilled or cancelled)");
|
|
338
338
|
}
|
|
@@ -377,7 +377,7 @@ function create(opts) {
|
|
|
377
377
|
"WHERE id = ?3 AND status = 'pending'",
|
|
378
378
|
[reason, ts, line.id],
|
|
379
379
|
);
|
|
380
|
-
if (
|
|
380
|
+
if (!b.sql.casWon(cancelClaim).won) {
|
|
381
381
|
throw new TypeError("backorder.cancelBackorder: line is no longer pending " +
|
|
382
382
|
"(concurrently fulfilled or cancelled)");
|
|
383
383
|
}
|
package/lib/carrier-accounts.js
CHANGED
|
@@ -591,7 +591,7 @@ function create(opts) {
|
|
|
591
591
|
"WHERE id = ?6 AND status IN ('active','rotating') AND api_key_hash = ?7",
|
|
592
592
|
[hashKey, prevHash, hashSec, now, now, id, prevHash],
|
|
593
593
|
);
|
|
594
|
-
if (
|
|
594
|
+
if (!b.sql.casWon(claim).won) {
|
|
595
595
|
var lost = await _getRaw(id);
|
|
596
596
|
var conflict = new Error(
|
|
597
597
|
(lost && lost.status === "disabled")
|
package/lib/cart-bulk-ops.js
CHANGED
|
@@ -536,7 +536,7 @@ function create(opts) {
|
|
|
536
536
|
"UPDATE carts SET status = 'abandoned', updated_at = ?1 WHERE id = ?2 AND status = 'active'",
|
|
537
537
|
[ts, input.cart_id],
|
|
538
538
|
);
|
|
539
|
-
if (
|
|
539
|
+
if (!b.sql.casWon(claim).won) {
|
|
540
540
|
throw new TypeError("cartBulkOps.splitCart: cart " + input.cart_id + " already split/modified");
|
|
541
541
|
}
|
|
542
542
|
var children = [];
|
package/lib/cart.js
CHANGED
|
@@ -450,7 +450,7 @@ function create(opts) {
|
|
|
450
450
|
[ts, cartId],
|
|
451
451
|
);
|
|
452
452
|
var cart = (await query("SELECT * FROM carts WHERE id = ?1", [cartId])).rows[0] || null;
|
|
453
|
-
return { claimed:
|
|
453
|
+
return { claimed: b.sql.casWon(r).won, cart: cart };
|
|
454
454
|
},
|
|
455
455
|
|
|
456
456
|
// Release a checkout claim back to 'active' so the buyer can retry. The
|
|
@@ -470,7 +470,7 @@ function create(opts) {
|
|
|
470
470
|
"UPDATE carts SET status = 'active', updated_at = ?1 WHERE id = ?2 AND status = 'converted'",
|
|
471
471
|
[ts, cartId],
|
|
472
472
|
);
|
|
473
|
-
return
|
|
473
|
+
return b.sql.casWon(r).won;
|
|
474
474
|
},
|
|
475
475
|
|
|
476
476
|
// ---- abandoned-cart visibility (operator dashboard) ---------------
|
package/lib/catalog.js
CHANGED
|
@@ -743,7 +743,7 @@ function _inventoryModule(query, opts) {
|
|
|
743
743
|
"WHERE sku = ?3 AND (stock_on_hand - stock_held) >= ?1",
|
|
744
744
|
[need, ts, sku],
|
|
745
745
|
);
|
|
746
|
-
if (rDown.
|
|
746
|
+
if (b.sql.casWon(rDown).won) {
|
|
747
747
|
await _afterMutation(sku);
|
|
748
748
|
return { adjusted: true, sku: sku, delta: delta };
|
|
749
749
|
}
|
|
@@ -796,7 +796,7 @@ function _inventoryModule(query, opts) {
|
|
|
796
796
|
"WHERE sku = ?3 AND (stock_on_hand - stock_held) >= ?1",
|
|
797
797
|
[qty, ts, sku],
|
|
798
798
|
);
|
|
799
|
-
if (r.
|
|
799
|
+
if (b.sql.casWon(r).won) {
|
|
800
800
|
// Hold lowers available; a low-stock threshold may now trip.
|
|
801
801
|
await _afterMutation(sku);
|
|
802
802
|
return { held: true, sku: sku, qty: qty };
|
|
@@ -829,7 +829,7 @@ function _inventoryModule(query, opts) {
|
|
|
829
829
|
"WHERE sku = ?3 AND stock_held >= ?1 AND stock_on_hand >= ?1",
|
|
830
830
|
[qty, ts, sku],
|
|
831
831
|
);
|
|
832
|
-
if (r.
|
|
832
|
+
if (b.sql.casWon(r).won) {
|
|
833
833
|
await _afterMutation(sku);
|
|
834
834
|
return { decremented: true, sku: sku, qty: qty };
|
|
835
835
|
}
|
package/lib/click-and-collect.js
CHANGED
|
@@ -493,7 +493,7 @@ function create(opts) {
|
|
|
493
493
|
input.scheduled_window_start, input.scheduled_window_end, now, now,
|
|
494
494
|
bucketStart, bucketEnd, capacity],
|
|
495
495
|
);
|
|
496
|
-
if (
|
|
496
|
+
if (!b.sql.casWon(ins).won) {
|
|
497
497
|
throw new TypeError("click-and-collect.scheduleAtLocation: location_code " +
|
|
498
498
|
JSON.stringify(input.location_code) + " is at capacity for the requested window");
|
|
499
499
|
}
|
|
@@ -592,7 +592,7 @@ function create(opts) {
|
|
|
592
592
|
"WHERE order_id = ?5 AND status = 'ready'",
|
|
593
593
|
[input.picked_up_at, sigHash, proofKind, now, orderId],
|
|
594
594
|
);
|
|
595
|
-
if (
|
|
595
|
+
if (!b.sql.casWon(pickupClaim).won) {
|
|
596
596
|
throw new TypeError("click-and-collect.markPickedUp: schedule is no longer ready " +
|
|
597
597
|
"(concurrently picked up)");
|
|
598
598
|
}
|
package/lib/customer-merge.js
CHANGED
|
@@ -707,7 +707,7 @@ function create(opts) {
|
|
|
707
707
|
"executed_at = ?1, executed_by = ?2 WHERE id = ?3 AND status = 'proposed'",
|
|
708
708
|
[ts, executedBy, mergeId],
|
|
709
709
|
);
|
|
710
|
-
if (
|
|
710
|
+
if (!b.sql.casWon(claim).won) {
|
|
711
711
|
// Lost the race (or the row left 'proposed' between our read
|
|
712
712
|
// and the claim). Re-read: an already-executed row is the
|
|
713
713
|
// winner's result; anything else is a genuine state error.
|
|
@@ -818,7 +818,7 @@ function create(opts) {
|
|
|
818
818
|
"AND EXISTS (SELECT 1 FROM customer_merge_redirects WHERE source_customer_id = ?4)",
|
|
819
819
|
[now, reason, mergeId, row.source_customer_id],
|
|
820
820
|
);
|
|
821
|
-
if (
|
|
821
|
+
if (!b.sql.casWon(claim).won) {
|
|
822
822
|
var raceErr = new Error("customerMerge.rollbackMerge: merge_id " + mergeId +
|
|
823
823
|
" is no longer executed (or its merge is still finalizing); only fully-executed merges can be rolled back");
|
|
824
824
|
raceErr.code = "CUSTOMER_MERGE_NOT_EXECUTED";
|
package/lib/customer-surveys.js
CHANGED
|
@@ -758,7 +758,7 @@ function create(opts) {
|
|
|
758
758
|
"WHERE id = ?2 AND status = 'issued'",
|
|
759
759
|
[nowTs, invitation.id],
|
|
760
760
|
);
|
|
761
|
-
if (
|
|
761
|
+
if (!b.sql.casWon(claim).won) {
|
|
762
762
|
var raced = new Error("customerSurveys.submitResponse: invitation already responded");
|
|
763
763
|
raced.code = "SURVEY_INVITATION_ALREADY_RESPONDED";
|
|
764
764
|
throw raced;
|
package/lib/cycle-counting.js
CHANGED
|
@@ -627,7 +627,7 @@ function create(opts) {
|
|
|
627
627
|
"WHERE slug = ?4 AND status IN ('scheduled', 'in_progress')",
|
|
628
628
|
[varianceCount, varianceValueMinor, ts, input.slug],
|
|
629
629
|
);
|
|
630
|
-
if (
|
|
630
|
+
if (!b.sql.casWon(claim).won) {
|
|
631
631
|
// Lost the claim — a concurrent (or prior) finalize already advanced
|
|
632
632
|
// this count and owns the adjustment writes. Touch no shelves; return
|
|
633
633
|
// the totals computed from the now-frozen lines.
|
|
@@ -358,7 +358,7 @@ function create(opts) {
|
|
|
358
358
|
" WHERE id = ?" + p + " AND status = ?" + (p + 1),
|
|
359
359
|
params,
|
|
360
360
|
);
|
|
361
|
-
if (
|
|
361
|
+
if (!b.sql.casWon(upd).won) {
|
|
362
362
|
var raced = new Error("dropship-forwarding: forwarding " + forwardingId +
|
|
363
363
|
" changed state concurrently (no longer " + current.status + ") — transition refused");
|
|
364
364
|
raced.code = "DROPSHIP_FORWARDING_TRANSITION_REFUSED";
|
package/lib/dunning.js
CHANGED
|
@@ -411,7 +411,7 @@ function create(opts) {
|
|
|
411
411
|
"WHERE id = ?1 AND status = 'active' AND next_action_at = ?2",
|
|
412
412
|
[enrollment.id, observedNextAt],
|
|
413
413
|
);
|
|
414
|
-
if (
|
|
414
|
+
if (!b.sql.casWon(claim).won) {
|
|
415
415
|
// Lost the race — another tick already claimed this row (or it
|
|
416
416
|
// was unset to a terminal status between the snapshot and now).
|
|
417
417
|
// Skip the side-effect; return the current row state.
|
package/lib/email-campaigns.js
CHANGED
|
@@ -853,7 +853,7 @@ function create(opts) {
|
|
|
853
853
|
"VALUES (?1, ?2, ?3, 'claimed', NULL, ?4)",
|
|
854
854
|
[b.uuid.v7(), slug, emailHash, Date.now()],
|
|
855
855
|
);
|
|
856
|
-
return r.
|
|
856
|
+
return b.sql.casWon(r).won;
|
|
857
857
|
}
|
|
858
858
|
|
|
859
859
|
async function _finalizeSend(slug, emailHash, outcome, failReason) {
|
|
@@ -1200,7 +1200,7 @@ function create(opts) {
|
|
|
1200
1200
|
"WHERE slug = ?2 AND status = 'scheduled'",
|
|
1201
1201
|
[Date.now(), slug],
|
|
1202
1202
|
);
|
|
1203
|
-
if (
|
|
1203
|
+
if (!b.sql.casWon(claim).won) {
|
|
1204
1204
|
var raceErr = new TypeError(
|
|
1205
1205
|
"emailCampaigns.sendNow: campaign '" + slug + "' is already being sent"
|
|
1206
1206
|
);
|
|
@@ -1260,7 +1260,7 @@ function create(opts) {
|
|
|
1260
1260
|
"WHERE slug = ?2 AND status = 'scheduled'",
|
|
1261
1261
|
[Date.now(), row.slug],
|
|
1262
1262
|
);
|
|
1263
|
-
if (
|
|
1263
|
+
if (!b.sql.casWon(claim).won) continue;
|
|
1264
1264
|
var stats = await _drainSend(row);
|
|
1265
1265
|
var midRow = await _getRow(row.slug);
|
|
1266
1266
|
await _fire(midRow, "complete");
|
|
@@ -1582,7 +1582,7 @@ function create(opts) {
|
|
|
1582
1582
|
"WHERE slug = ?2 AND status = 'scheduled'",
|
|
1583
1583
|
[Date.now(), slug],
|
|
1584
1584
|
);
|
|
1585
|
-
if (
|
|
1585
|
+
if (!b.sql.casWon(claim).won) {
|
|
1586
1586
|
var raceErr = new TypeError(
|
|
1587
1587
|
"emailCampaigns.broadcast: campaign '" + slug + "' is already being sent"
|
|
1588
1588
|
);
|
package/lib/giftcards.js
CHANGED
|
@@ -322,7 +322,7 @@ function create(opts) {
|
|
|
322
322
|
"updated_at = ?2 WHERE id = ?3 AND balance_minor >= ?1 AND status = 'active'",
|
|
323
323
|
[input.amount_minor, ts, row.id],
|
|
324
324
|
);
|
|
325
|
-
if (dec.
|
|
325
|
+
if (!b.sql.casWon(dec).won) {
|
|
326
326
|
// Race: another redemption beat us to the balance. Refuse
|
|
327
327
|
// with the same shape as the up-front insufficient check so
|
|
328
328
|
// the caller doesn't have to distinguish "checked then
|
|
@@ -407,7 +407,7 @@ function create(opts) {
|
|
|
407
407
|
"UPDATE giftcard_redemptions SET reversed_at = ?1 WHERE id = ?2 AND reversed_at IS NULL",
|
|
408
408
|
[ts, red.id],
|
|
409
409
|
);
|
|
410
|
-
if (
|
|
410
|
+
if (!b.sql.casWon(claim).won) continue; // lost the claim
|
|
411
411
|
// Restore the spendable balance on the card row. Capped at the card's
|
|
412
412
|
// issued_minor by the schema CHECK; the amount restored is exactly
|
|
413
413
|
// what this redemption debited, so it can't exceed the face value.
|
|
@@ -446,7 +446,7 @@ function create(opts) {
|
|
|
446
446
|
"UPDATE giftcard_redemptions SET order_id = ?1 WHERE id = ?2 AND order_id IS NULL",
|
|
447
447
|
[orderId, redemptionId],
|
|
448
448
|
);
|
|
449
|
-
return
|
|
449
|
+
return b.sql.casWon(res).won;
|
|
450
450
|
},
|
|
451
451
|
|
|
452
452
|
// Reverse ONE redemption by its own id — the compensation edge for a
|
|
@@ -472,7 +472,7 @@ function create(opts) {
|
|
|
472
472
|
"UPDATE giftcard_redemptions SET reversed_at = ?1 WHERE id = ?2 AND reversed_at IS NULL",
|
|
473
473
|
[ts, red.id],
|
|
474
474
|
);
|
|
475
|
-
if (
|
|
475
|
+
if (!b.sql.casWon(claim).won) return null; // lost the claim
|
|
476
476
|
await query(
|
|
477
477
|
"UPDATE giftcards SET balance_minor = balance_minor + ?1, " +
|
|
478
478
|
"status = CASE WHEN status = 'redeemed' THEN 'active' ELSE status END, " +
|
|
@@ -554,7 +554,7 @@ function create(opts) {
|
|
|
554
554
|
"WHERE id = ?4 AND reversed_minor = ?5",
|
|
555
555
|
[target, nowFull ? 1 : 0, ts, red.id, already],
|
|
556
556
|
);
|
|
557
|
-
if (
|
|
557
|
+
if (!b.sql.casWon(claim).won) continue; // lost the claim to a concurrent reversal
|
|
558
558
|
// Restore the spendable balance on the card row. Capped at the card's
|
|
559
559
|
// issued_minor by the schema CHECK; the cumulative reversed_minor is
|
|
560
560
|
// bounded by amount_minor, so it can't exceed the face value.
|
|
@@ -592,7 +592,7 @@ function create(opts) {
|
|
|
592
592
|
// the row (no schema column) but accepted so a future audit-log
|
|
593
593
|
// primitive can ride alongside without a surface change.
|
|
594
594
|
void opts2.reason;
|
|
595
|
-
if (
|
|
595
|
+
if (b.sql.casWon(upd).won) {
|
|
596
596
|
var after = await query("SELECT * FROM giftcards WHERE id = ?1", [id]);
|
|
597
597
|
return after.rows[0] || null;
|
|
598
598
|
}
|
|
@@ -381,7 +381,7 @@ function create(opts) {
|
|
|
381
381
|
"committed_order_id = ?2 WHERE id = ?3 AND status = 'held'",
|
|
382
382
|
[ts, input.order_id, input.hold_id],
|
|
383
383
|
);
|
|
384
|
-
if (
|
|
384
|
+
if (!b.sql.casWon(claim).won) {
|
|
385
385
|
throw new TypeError("inventory-allocations.commitHold: hold " + existing.id +
|
|
386
386
|
" is no longer held (committed by a concurrent call)");
|
|
387
387
|
}
|
package/lib/inventory-audits.js
CHANGED
|
@@ -642,7 +642,7 @@ function create(opts) {
|
|
|
642
642
|
"WHERE id = ?4 AND status IN ('open', 'in_progress')",
|
|
643
643
|
[varianceCount, varianceValueMinor, ts, auditId],
|
|
644
644
|
);
|
|
645
|
-
if (
|
|
645
|
+
if (!b.sql.casWon(claim).won) {
|
|
646
646
|
// Lost the claim — a concurrent (or prior) finalize already
|
|
647
647
|
// advanced this audit and owns the adjustment writes. Touch no
|
|
648
648
|
// shelves; return the totals computed from the now-frozen lines.
|
package/lib/inventory-receive.js
CHANGED
|
@@ -351,7 +351,7 @@ function create(opts) {
|
|
|
351
351
|
"UPDATE inventory_receipts SET status = 'applied', updated_at = ?1 WHERE id = ?2 AND status = 'pending'",
|
|
352
352
|
[ts, receiptId],
|
|
353
353
|
);
|
|
354
|
-
if (
|
|
354
|
+
if (!b.sql.casWon(claim).won) {
|
|
355
355
|
// A concurrent call won the claim and is restocking (or already did).
|
|
356
356
|
// Replay-safe no-op so the loser doesn't double-apply.
|
|
357
357
|
return { id: receiptId, applied_count: 0, stock_changes: [] };
|
|
@@ -422,7 +422,7 @@ function create(opts) {
|
|
|
422
422
|
"UPDATE inventory_receipts SET status = 'reversed', updated_at = ?1 WHERE id = ?2 AND status = 'applied'",
|
|
423
423
|
[claimTs, receiptId],
|
|
424
424
|
);
|
|
425
|
-
if (
|
|
425
|
+
if (!b.sql.casWon(claim).won) {
|
|
426
426
|
throw new TypeError("inventory-receive.reverse: receipt " + receiptId +
|
|
427
427
|
" is no longer applied (reversed by a concurrent call)");
|
|
428
428
|
}
|
|
@@ -555,7 +555,7 @@ function create(opts) {
|
|
|
555
555
|
"reverse_reason = ?2 WHERE id = ?3 AND status = 'recorded'",
|
|
556
556
|
[ts, reason, id],
|
|
557
557
|
);
|
|
558
|
-
if (
|
|
558
|
+
if (!b.sql.casWon(claim).won) {
|
|
559
559
|
throw new TypeError("inventory-writeoffs.reverseWriteoff: writeoff " + id +
|
|
560
560
|
" is no longer recorded (reversed by a concurrent call)");
|
|
561
561
|
}
|
package/lib/invoice-renderer.js
CHANGED
|
@@ -319,7 +319,7 @@ function create(opts) {
|
|
|
319
319
|
"WHERE series = ?3 AND next_value = ?4",
|
|
320
320
|
[current + 1, Date.now(), series, current],
|
|
321
321
|
);
|
|
322
|
-
if (
|
|
322
|
+
if (b.sql.casWon(info).won) return current;
|
|
323
323
|
}
|
|
324
324
|
throw new Error("invoiceRenderer: sequence advance for series " + JSON.stringify(series) + " contended 16 times");
|
|
325
325
|
}
|
package/lib/knowledge-base.js
CHANGED
|
@@ -928,7 +928,7 @@ function create(opts) {
|
|
|
928
928
|
"VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
929
929
|
[b.uuid.v7(), slug, sessionHash, vote, ts],
|
|
930
930
|
);
|
|
931
|
-
var changed = inserted && (inserted.
|
|
931
|
+
var changed = inserted && b.sql.casWon(inserted).won;
|
|
932
932
|
if (changed) {
|
|
933
933
|
var col = vote === "helpful" ? "helpful_count" : "not_helpful_count";
|
|
934
934
|
await query(
|
|
@@ -533,7 +533,7 @@ function create(opts) {
|
|
|
533
533
|
"WHERE customer_id = ?2 AND reward_slug = ?3 AND status IN ('active','consumed')) < ?7",
|
|
534
534
|
[redemptionId, customerId, rewardSlug, reward.point_cost, ts, expiresAt, reward.max_per_customer],
|
|
535
535
|
);
|
|
536
|
-
if (
|
|
536
|
+
if (!b.sql.casWon(claim).won) {
|
|
537
537
|
// Lost the cap claim — another redeem filled the last slot between
|
|
538
538
|
// the pre-check and here. Return the points and refuse, so a raced
|
|
539
539
|
// caller is never charged for a redemption it didn't get.
|
|
@@ -648,7 +648,7 @@ function create(opts) {
|
|
|
648
648
|
"WHERE id = ?3 AND status = 'active'",
|
|
649
649
|
[ts, orderId, redemptionId],
|
|
650
650
|
);
|
|
651
|
-
if (
|
|
651
|
+
if (!b.sql.casWon(r).won) {
|
|
652
652
|
// Lost the race — re-read so the caller gets the row's actual
|
|
653
653
|
// post-race state. Refuse with the same FSM error so the call
|
|
654
654
|
// surface is identical whether the loser noticed pre- or
|
|
@@ -691,7 +691,7 @@ function create(opts) {
|
|
|
691
691
|
"WHERE id = ?2 AND status = 'active'",
|
|
692
692
|
[reason, redemptionId],
|
|
693
693
|
);
|
|
694
|
-
if (
|
|
694
|
+
if (!b.sql.casWon(r).won) {
|
|
695
695
|
var raceAfter = await getRedemption(redemptionId);
|
|
696
696
|
var raceErr = new Error("loyaltyRedemption.cancelRedemption: redemption status is '" + (raceAfter && raceAfter.status) + "', only 'active' may be cancelled with a point refund");
|
|
697
697
|
raceErr.code = "REDEMPTION_NOT_ACTIVE";
|
package/lib/loyalty.js
CHANGED
|
@@ -329,7 +329,7 @@ function create(opts) {
|
|
|
329
329
|
"updated_at = ?2 WHERE customer_id = ?3 AND balance_points >= ?1",
|
|
330
330
|
[points, ts, customerId],
|
|
331
331
|
);
|
|
332
|
-
if (dec.
|
|
332
|
+
if (!b.sql.casWon(dec).won) {
|
|
333
333
|
var raced = new Error("loyalty.redeem: insufficient balance");
|
|
334
334
|
raced.code = "LOYALTY_INSUFFICIENT_BALANCE";
|
|
335
335
|
throw raced;
|
|
@@ -366,7 +366,7 @@ function create(opts) {
|
|
|
366
366
|
"WHERE id = ?2 AND transaction_type = 'redeem' AND order_id IS NULL",
|
|
367
367
|
[oid, tid],
|
|
368
368
|
);
|
|
369
|
-
return
|
|
369
|
+
return b.sql.casWon(res).won;
|
|
370
370
|
},
|
|
371
371
|
|
|
372
372
|
// Reverse ONE redeem burn by its ledger row id — the compensation edge
|
|
@@ -394,7 +394,7 @@ function create(opts) {
|
|
|
394
394
|
"WHERE id = ?2 AND restored_points = 0",
|
|
395
395
|
[spent, row.id],
|
|
396
396
|
);
|
|
397
|
-
if (
|
|
397
|
+
if (!b.sql.casWon(claim).won) return { restored_points: 0 }; // lost the claim
|
|
398
398
|
await _ensureAccountRow(row.customer_id, ts);
|
|
399
399
|
await query(
|
|
400
400
|
"UPDATE loyalty_accounts SET balance_points = balance_points + ?1, " +
|
|
@@ -473,7 +473,7 @@ function create(opts) {
|
|
|
473
473
|
"WHERE id = ?2 AND restored_points = ?3",
|
|
474
474
|
[target, row.id, already],
|
|
475
475
|
);
|
|
476
|
-
if (
|
|
476
|
+
if (!b.sql.casWon(claim).won) continue; // lost the claim
|
|
477
477
|
await _ensureAccountRow(row.customer_id, ts);
|
|
478
478
|
// Credit BALANCE only — lifetime is untouched (a redeem never moved
|
|
479
479
|
// lifetime; restoring it mustn't either). Relative-atomic so a
|
|
@@ -529,7 +529,7 @@ function create(opts) {
|
|
|
529
529
|
"updated_at = ?3 WHERE customer_id = ?4 AND balance_points + ?1 >= 0",
|
|
530
530
|
[delta, lifetimeDelta, ts, customerId],
|
|
531
531
|
);
|
|
532
|
-
if (
|
|
532
|
+
if (!b.sql.casWon(upd).won) {
|
|
533
533
|
var ins = new Error("loyalty.adjust: adjustment would underflow balance");
|
|
534
534
|
ins.code = "LOYALTY_INSUFFICIENT_BALANCE";
|
|
535
535
|
throw ins;
|
|
@@ -737,7 +737,7 @@ function create(opts) {
|
|
|
737
737
|
"executed_at = ?2, result_json = ?3, updated_at = ?2 WHERE id = ?4 AND status = 'approved'",
|
|
738
738
|
[executedBy, ts, resultJson, requestId],
|
|
739
739
|
);
|
|
740
|
-
if (
|
|
740
|
+
if (!b.sql.casWon(claim).won) {
|
|
741
741
|
var raced = new Error("operatorApprovals.markExecuted: request " +
|
|
742
742
|
JSON.stringify(requestId) + " is no longer approved — execution refused");
|
|
743
743
|
raced.code = "APPROVAL_EXECUTE_RACE";
|
|
@@ -844,7 +844,7 @@ function create(opts) {
|
|
|
844
844
|
"VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
845
845
|
[b.uuid.v7(), slug, operatorId, vote, ts],
|
|
846
846
|
);
|
|
847
|
-
var changed = inserted && (
|
|
847
|
+
var changed = inserted && (b.sql.casWon(inserted).won);
|
|
848
848
|
if (changed) {
|
|
849
849
|
var col = vote === "helpful" ? "helpful_count" : "not_helpful_count";
|
|
850
850
|
await query(
|
package/lib/order-exchanges.js
CHANGED
|
@@ -300,7 +300,7 @@ function create(opts) {
|
|
|
300
300
|
"WHERE id = ?3 AND status = 'pending'",
|
|
301
301
|
[approverId, ts, exchangeId],
|
|
302
302
|
);
|
|
303
|
-
if (
|
|
303
|
+
if (!b.sql.casWon(claim).won) {
|
|
304
304
|
var refused = new Error(
|
|
305
305
|
"order-exchanges: transition 'approveExchange' refused from state '" +
|
|
306
306
|
existing.status + "' (lost the race to a concurrent transition)"
|
package/lib/order-export.js
CHANGED
|
@@ -600,7 +600,7 @@ function create(opts) {
|
|
|
600
600
|
"WHERE id = ?2 AND status = ?3",
|
|
601
601
|
[ts, exportId, row.status],
|
|
602
602
|
);
|
|
603
|
-
if (
|
|
603
|
+
if (!b.sql.casWon(upd).won) {
|
|
604
604
|
var raced = new Error("order-export.cancelExport: " + exportId +
|
|
605
605
|
" changed state concurrently (no longer " + row.status + ") — refused");
|
|
606
606
|
raced.code = "ORDER_EXPORT_CANCEL_REFUSED";
|
|
@@ -664,7 +664,7 @@ function create(opts) {
|
|
|
664
664
|
"WHERE id = ?2 AND status = ?3",
|
|
665
665
|
[ts, exportId, row.status],
|
|
666
666
|
);
|
|
667
|
-
if (
|
|
667
|
+
if (!b.sql.casWon(upd).won) {
|
|
668
668
|
var raced = new Error("order-export.markExportRunning: " + exportId +
|
|
669
669
|
" changed state concurrently (no longer " + row.status + ") — refused");
|
|
670
670
|
raced.code = "ORDER_EXPORT_START_REFUSED";
|
|
@@ -703,7 +703,7 @@ function create(opts) {
|
|
|
703
703
|
"byte_size = ?2, file_sha3_512 = ?3, completed_at = ?4 WHERE id = ?5 AND status = ?6",
|
|
704
704
|
[input.row_count, input.byte_size, input.file_sha3_512, ts, input.export_id, row.status],
|
|
705
705
|
);
|
|
706
|
-
if (
|
|
706
|
+
if (!b.sql.casWon(upd).won) {
|
|
707
707
|
var raced = new Error("order-export.markExportComplete: " + input.export_id +
|
|
708
708
|
" changed state concurrently (no longer " + row.status + ") — refused");
|
|
709
709
|
raced.code = "ORDER_EXPORT_COMPLETE_REFUSED";
|
|
@@ -744,7 +744,7 @@ function create(opts) {
|
|
|
744
744
|
"WHERE id = ?3 AND status = ?4",
|
|
745
745
|
[errorText, ts, input.export_id, row.status],
|
|
746
746
|
);
|
|
747
|
-
if (
|
|
747
|
+
if (!b.sql.casWon(upd).won) {
|
|
748
748
|
var raced = new Error("order-export.markExportFailed: " + input.export_id +
|
|
749
749
|
" changed state concurrently (no longer " + row.status + ") — refused");
|
|
750
750
|
raced.code = "ORDER_EXPORT_FAIL_REFUSED";
|
package/lib/payment-retries.js
CHANGED
|
@@ -602,7 +602,7 @@ function create(opts) {
|
|
|
602
602
|
"WHERE id = ?1 AND status = 'active' AND next_retry_at = ?2",
|
|
603
603
|
[enrollment.id, enrollment.next_retry_at],
|
|
604
604
|
);
|
|
605
|
-
if (
|
|
605
|
+
if (!b.sql.casWon(claim).won) continue; // another tick claimed it
|
|
606
606
|
|
|
607
607
|
// Pre-charge prep. The claim above nulled next_retry_at to take
|
|
608
608
|
// the row out of the due set; a throw HERE (a missing policy /
|
|
@@ -707,7 +707,7 @@ function create(opts) {
|
|
|
707
707
|
"WHERE id = ?2 AND status = 'active' AND attempt_count = ?3",
|
|
708
708
|
[nextAttempt, enrollment.id, attemptCount],
|
|
709
709
|
);
|
|
710
|
-
if (
|
|
710
|
+
if (!b.sql.casWon(outcomeClaim).won) {
|
|
711
711
|
return await _refetchEnrollment(enrollment.id);
|
|
712
712
|
}
|
|
713
713
|
|
package/lib/pick-lists.js
CHANGED
|
@@ -565,7 +565,7 @@ function create(opts) {
|
|
|
565
565
|
"WHERE id = ?2 AND status IN ('generated', 'in_progress')",
|
|
566
566
|
[ts, listId],
|
|
567
567
|
);
|
|
568
|
-
if (
|
|
568
|
+
if (!b.sql.casWon(claim).won) {
|
|
569
569
|
// Lost the race (or the row left an eligible state between the
|
|
570
570
|
// read and the claim). Surface the same terminal-state refusal
|
|
571
571
|
// a sequential caller would see — the shipments belong to the
|
package/lib/plan-changes.js
CHANGED
|
@@ -485,7 +485,7 @@ function create(opts) {
|
|
|
485
485
|
"WHERE id = ?2 AND status = 'pending'",
|
|
486
486
|
[now, row.id],
|
|
487
487
|
);
|
|
488
|
-
if (
|
|
488
|
+
if (!b.sql.casWon(claim).won) continue; // another run claimed it
|
|
489
489
|
|
|
490
490
|
await query(
|
|
491
491
|
"UPDATE subscriptions SET plan_id = ?1, updated_at = ?2 WHERE id = ?3",
|
package/lib/preorder.js
CHANGED
|
@@ -265,7 +265,7 @@ function create(opts) {
|
|
|
265
265
|
"WHERE id = ?2 AND status = 'active'",
|
|
266
266
|
[ts, reservation.id],
|
|
267
267
|
);
|
|
268
|
-
if (
|
|
268
|
+
if (!b.sql.casWon(claim).won) {
|
|
269
269
|
// Lost the race — a concurrent converter already claimed this
|
|
270
270
|
// reservation. Skip the side-effect; the winner mints the order.
|
|
271
271
|
return { won: false, orderId: null };
|
|
@@ -436,7 +436,7 @@ function create(opts) {
|
|
|
436
436
|
"AND (max_units_available IS NULL OR units_reserved + ?1 <= max_units_available)",
|
|
437
437
|
[input.quantity, ts, input.campaign_slug],
|
|
438
438
|
);
|
|
439
|
-
if (
|
|
439
|
+
if (!b.sql.casWon(claim).won) {
|
|
440
440
|
throw new TypeError("preorder.reserve: would exceed max_units_available " +
|
|
441
441
|
"(cap=" + campaign.max_units_available + ", reserved=" + campaign.units_reserved +
|
|
442
442
|
", requested=" + input.quantity + ")");
|
|
@@ -529,7 +529,7 @@ function create(opts) {
|
|
|
529
529
|
"cancel_reason = ?2 WHERE id = ?3 AND status = 'active'",
|
|
530
530
|
[ts, reason, reservation.id],
|
|
531
531
|
);
|
|
532
|
-
if (
|
|
532
|
+
if (!b.sql.casWon(claim).won) {
|
|
533
533
|
throw new TypeError("preorder.cancelReservation: reservation " +
|
|
534
534
|
JSON.stringify(reservation.id) + " was already cancelled or converted concurrently");
|
|
535
535
|
}
|
|
@@ -621,7 +621,7 @@ function create(opts) {
|
|
|
621
621
|
"WHERE slug = ?2 AND status = 'active'",
|
|
622
622
|
[ts, input.slug],
|
|
623
623
|
);
|
|
624
|
-
if (
|
|
624
|
+
if (!b.sql.casWon(claim).won) {
|
|
625
625
|
throw new TypeError("preorder.launchCampaign: campaign " +
|
|
626
626
|
JSON.stringify(input.slug) + " was already launched concurrently");
|
|
627
627
|
}
|
package/lib/print-on-demand.js
CHANGED
|
@@ -332,7 +332,7 @@ function create(opts) {
|
|
|
332
332
|
" WHERE id = ?" + p + " AND status = ?" + (p + 1),
|
|
333
333
|
params,
|
|
334
334
|
);
|
|
335
|
-
if (
|
|
335
|
+
if (!b.sql.casWon(upd).won) {
|
|
336
336
|
var raced = new Error("print-on-demand: fulfillment " + fulfillmentId +
|
|
337
337
|
" changed state concurrently (no longer " + current.status + ") — transition refused");
|
|
338
338
|
raced.code = "POD_FULFILLMENT_TRANSITION_REFUSED";
|
package/lib/print-queue.js
CHANGED
|
@@ -374,7 +374,7 @@ function create(opts) {
|
|
|
374
374
|
"claimed_at = ?2 WHERE id = ?3 AND status = 'queued'",
|
|
375
375
|
[input.station_id, ts, jobId],
|
|
376
376
|
);
|
|
377
|
-
if (upd.
|
|
377
|
+
if (!b.sql.casWon(upd).won) return null;
|
|
378
378
|
return await _getRow(jobId);
|
|
379
379
|
},
|
|
380
380
|
|
|
@@ -452,7 +452,7 @@ function create(opts) {
|
|
|
452
452
|
[ts, reason, jobId],
|
|
453
453
|
);
|
|
454
454
|
var failed = await _getRow(jobId);
|
|
455
|
-
if (
|
|
455
|
+
if (!b.sql.casWon(upd).won) {
|
|
456
456
|
// Another caller already transitioned this job out of
|
|
457
457
|
// in_progress. Return its current state; do not re-insert a
|
|
458
458
|
// retry row (that's the exactly-once side-effect we're
|
package/lib/purchase-orders.js
CHANGED
|
@@ -643,7 +643,7 @@ function create(opts) {
|
|
|
643
643
|
[cAdd, ts, cLine.id, cPrior],
|
|
644
644
|
);
|
|
645
645
|
}
|
|
646
|
-
if (
|
|
646
|
+
if (!b.sql.casWon(lineClaim).won) {
|
|
647
647
|
// Lost the race on this line (a concurrent receipt already
|
|
648
648
|
// advanced it past the value we observed) — roll back the
|
|
649
649
|
// line claims we already won this call, self-targeting the
|
|
@@ -823,7 +823,7 @@ function create(opts) {
|
|
|
823
823
|
"failed_at = ?2 WHERE id = ?3 AND status = 'queued'",
|
|
824
824
|
["provider_unavailable_at_dispatch", now, row.id],
|
|
825
825
|
);
|
|
826
|
-
if (
|
|
826
|
+
if (!b.sql.casWon(lostFail).won) continue;
|
|
827
827
|
advanced.push(await _getNotification(row.id));
|
|
828
828
|
continue;
|
|
829
829
|
}
|
|
@@ -834,7 +834,7 @@ function create(opts) {
|
|
|
834
834
|
"WHERE id = ?2 AND status = 'queued'",
|
|
835
835
|
[now, row.id],
|
|
836
836
|
);
|
|
837
|
-
if (
|
|
837
|
+
if (!b.sql.casWon(claimed).won) continue;
|
|
838
838
|
advanced.push(await _getNotification(row.id));
|
|
839
839
|
}
|
|
840
840
|
return advanced;
|
package/lib/quotes.js
CHANGED
|
@@ -683,7 +683,7 @@ function create(opts) {
|
|
|
683
683
|
[shipping, tax, total, currency, validUntil, operatorNotes,
|
|
684
684
|
delivery, payment, ts, quoteId, fromStatus],
|
|
685
685
|
);
|
|
686
|
-
if (
|
|
686
|
+
if (!b.sql.casWon(claim).won) {
|
|
687
687
|
var raced = new Error("quotes." + verbLabel + ": refused — quote " + quoteId +
|
|
688
688
|
" is no longer in the " + fromStatus + " state (settled by a concurrent call)");
|
|
689
689
|
raced.code = "QUOTE_TRANSITION_REFUSED";
|
|
@@ -880,7 +880,7 @@ function create(opts) {
|
|
|
880
880
|
"accepted_by_customer = ?2, updated_at = ?1 WHERE id = ?3 AND status = 'responded'",
|
|
881
881
|
[ts, acceptedBy, quoteId],
|
|
882
882
|
);
|
|
883
|
-
if (
|
|
883
|
+
if (!b.sql.casWon(claim).won) {
|
|
884
884
|
var raced = new Error("quotes.customerAccept: refused — quote " + quoteId +
|
|
885
885
|
" is no longer responded (accepted, rejected, or withdrawn by a concurrent call)");
|
|
886
886
|
raced.code = "QUOTE_TRANSITION_REFUSED";
|
|
@@ -913,7 +913,7 @@ function create(opts) {
|
|
|
913
913
|
"reject_reason = ?2, updated_at = ?1 WHERE id = ?3 AND status = 'responded'",
|
|
914
914
|
[ts, reason, quoteId],
|
|
915
915
|
);
|
|
916
|
-
if (
|
|
916
|
+
if (!b.sql.casWon(claim).won) {
|
|
917
917
|
var raced = new Error("quotes.customerReject: refused — quote " + quoteId +
|
|
918
918
|
" is no longer responded (settled by a concurrent call)");
|
|
919
919
|
raced.code = "QUOTE_TRANSITION_REFUSED";
|
|
@@ -954,7 +954,7 @@ function create(opts) {
|
|
|
954
954
|
"cancel_reason = ?2, updated_at = ?1 WHERE id = ?3 AND status IN ('requested', 'responded')",
|
|
955
955
|
[ts, reason, quoteId],
|
|
956
956
|
);
|
|
957
|
-
if (
|
|
957
|
+
if (!b.sql.casWon(claim).won) {
|
|
958
958
|
var raced = new Error("quotes.cancelQuote: refused — quote " + quoteId +
|
|
959
959
|
" is no longer cancellable (accepted, rejected, or settled by a concurrent call)");
|
|
960
960
|
raced.code = "QUOTE_TRANSITION_REFUSED";
|
|
@@ -998,7 +998,7 @@ function create(opts) {
|
|
|
998
998
|
"WHERE id = ?2 AND status = 'accepted'",
|
|
999
999
|
[ts, quoteId],
|
|
1000
1000
|
);
|
|
1001
|
-
if (
|
|
1001
|
+
if (!b.sql.casWon(claim).won) {
|
|
1002
1002
|
var raced = new Error("quotes.convertToOrder: refused — quote " + quoteId +
|
|
1003
1003
|
" is no longer accepted (converted by a concurrent call)");
|
|
1004
1004
|
raced.code = "QUOTE_TRANSITION_REFUSED";
|
|
@@ -1326,7 +1326,7 @@ function create(opts) {
|
|
|
1326
1326
|
"AND valid_until IS NOT NULL AND valid_until <= ?3",
|
|
1327
1327
|
[asOf, r.rows[i].id, asOf],
|
|
1328
1328
|
);
|
|
1329
|
-
if (
|
|
1329
|
+
if (b.sql.casWon(claim).won) expired += 1;
|
|
1330
1330
|
else skipped += 1;
|
|
1331
1331
|
}
|
|
1332
1332
|
return { scanned: r.rows.length, expired: expired, skipped: skipped };
|
|
@@ -1361,7 +1361,7 @@ function create(opts) {
|
|
|
1361
1361
|
"UPDATE quotes SET status = 'expired', updated_at = ?1 WHERE id = ?2 AND status = 'responded'",
|
|
1362
1362
|
[asOf, quoteId],
|
|
1363
1363
|
);
|
|
1364
|
-
if (
|
|
1364
|
+
if (!b.sql.casWon(claim).won) {
|
|
1365
1365
|
var raced = new Error("quotes.markExpired: refused — quote " + quoteId +
|
|
1366
1366
|
" is no longer responded (settled by a concurrent call)");
|
|
1367
1367
|
raced.code = "QUOTE_TRANSITION_REFUSED";
|
package/lib/referrals.js
CHANGED
|
@@ -409,7 +409,7 @@ function create(opts) {
|
|
|
409
409
|
"WHERE id = ?3 AND signed_up_customer_id IS NULL",
|
|
410
410
|
[ts, customerId, invitationId],
|
|
411
411
|
);
|
|
412
|
-
if (
|
|
412
|
+
if (!b.sql.casWon(claim).won) continue;
|
|
413
413
|
var updated = await query(
|
|
414
414
|
"SELECT id, referral_code_id, signed_up_customer_id, signed_up_at, reward_status " +
|
|
415
415
|
"FROM referral_invitations WHERE id = ?1",
|
|
@@ -466,7 +466,7 @@ function create(opts) {
|
|
|
466
466
|
"WHERE id = ?3 AND first_purchase_at IS NULL",
|
|
467
467
|
[ts, orderId, inv.id],
|
|
468
468
|
);
|
|
469
|
-
if (
|
|
469
|
+
if (!b.sql.casWon(claim).won) {
|
|
470
470
|
// Lost the claim — another concurrent call already recorded the
|
|
471
471
|
// first qualifying purchase. Re-read so the loser reports the
|
|
472
472
|
// winner's pinned values, not a stale snapshot.
|
|
@@ -554,7 +554,7 @@ function create(opts) {
|
|
|
554
554
|
"WHERE id = ?2 AND reversed_at IS NULL",
|
|
555
555
|
[ts, inv.id],
|
|
556
556
|
);
|
|
557
|
-
if (
|
|
557
|
+
if (!b.sql.casWon(claim).won) return null;
|
|
558
558
|
// Decrement the referrer's leaderboard count, floored at zero (the
|
|
559
559
|
// `> 0` guard plus the schema CHECK keep it non-negative even if the
|
|
560
560
|
// count drifted).
|
package/lib/reorder-reminders.js
CHANGED
|
@@ -536,7 +536,7 @@ function create(opts) {
|
|
|
536
536
|
"WHERE id = ?2 AND status = 'active' AND next_remind_at = ?3",
|
|
537
537
|
[nextRemindAt, enrollment.id, enrollment.next_remind_at],
|
|
538
538
|
);
|
|
539
|
-
if (
|
|
539
|
+
if (!b.sql.casWon(claim).won) {
|
|
540
540
|
// Another tick already advanced this row's cursor (or it was
|
|
541
541
|
// cancelled between the SELECT and now) — it owns the send.
|
|
542
542
|
// Skip without dispatching so the customer is nudged once.
|
package/lib/returns.js
CHANGED
|
@@ -348,7 +348,7 @@ function create(opts) {
|
|
|
348
348
|
"updated_at = ?3 WHERE id = ?5 AND status = 'pending'",
|
|
349
349
|
[input.refund_amount_minor, refundCurrency, ts, operatorNotes, rmaId],
|
|
350
350
|
);
|
|
351
|
-
if (
|
|
351
|
+
if (!b.sql.casWon(upd).won) {
|
|
352
352
|
var current = await this._currentStatus(rmaId);
|
|
353
353
|
_assertTransition(current, "approve");
|
|
354
354
|
var raced = new Error("returns: approve already claimed for rma " + rmaId);
|
|
@@ -376,7 +376,7 @@ function create(opts) {
|
|
|
376
376
|
"updated_at = ?3 WHERE id = ?4 AND status = 'approved'",
|
|
377
377
|
[rxAt, operatorNotes, ts, rmaId],
|
|
378
378
|
);
|
|
379
|
-
if (
|
|
379
|
+
if (!b.sql.casWon(upd).won) {
|
|
380
380
|
var current = await this._currentStatus(rmaId);
|
|
381
381
|
_assertTransition(current, "markReceived");
|
|
382
382
|
var raced = new Error("returns: markReceived already claimed for rma " + rmaId);
|
|
@@ -412,7 +412,7 @@ function create(opts) {
|
|
|
412
412
|
"updated_at = ?3 WHERE id = ?4 AND status = 'received'",
|
|
413
413
|
[rfAt, operatorNotes, ts, rmaId],
|
|
414
414
|
);
|
|
415
|
-
if (
|
|
415
|
+
if (!b.sql.casWon(upd).won) {
|
|
416
416
|
// Either the RMA doesn't exist or it isn't in 'received'. Read the
|
|
417
417
|
// current status to surface the right typed refusal (RMA_NOT_FOUND
|
|
418
418
|
// vs RMA_TRANSITION_REFUSED) so the caller's mapping is unchanged.
|
|
@@ -444,7 +444,7 @@ function create(opts) {
|
|
|
444
444
|
"updated_at = ?1 WHERE id = ?2 AND status = 'refunded'",
|
|
445
445
|
[ts, rmaId],
|
|
446
446
|
);
|
|
447
|
-
return
|
|
447
|
+
return b.sql.casWon(upd).won;
|
|
448
448
|
},
|
|
449
449
|
|
|
450
450
|
reject: async function (rmaId, input) {
|
package/lib/seller-signup.js
CHANGED
|
@@ -874,7 +874,7 @@ function create(opts) {
|
|
|
874
874
|
"OR (status = 'approved' AND vendor_slug IS NULL AND approved_at = ?4))",
|
|
875
875
|
[ts, approvedBy, applicationId, app.approved_at],
|
|
876
876
|
);
|
|
877
|
-
if (
|
|
877
|
+
if (!b.sql.casWon(claim).won) {
|
|
878
878
|
var raced = new Error(
|
|
879
879
|
"seller-signup.approveApplication: refused — application is not in an approvable state (concurrent approval)"
|
|
880
880
|
);
|
|
@@ -621,7 +621,7 @@ function create(opts) {
|
|
|
621
621
|
"resolved_at = ?2, updated_at = ?2 WHERE id = ?3 AND status = 'filed'",
|
|
622
622
|
[input.payout_minor, now, claimId],
|
|
623
623
|
);
|
|
624
|
-
if (
|
|
624
|
+
if (!b.sql.casWon(approvedUpd).won) {
|
|
625
625
|
var racedApprove = await _getClaimRow(claimId);
|
|
626
626
|
throw new TypeError("shippingInsurance.markClaimApproved: claim " + claimId +
|
|
627
627
|
" is " + (racedApprove ? racedApprove.status : "gone") +
|
|
@@ -658,7 +658,7 @@ function create(opts) {
|
|
|
658
658
|
"resolved_at = ?2, updated_at = ?2 WHERE id = ?3 AND status = 'filed'",
|
|
659
659
|
[denialReason, now, claimId],
|
|
660
660
|
);
|
|
661
|
-
if (
|
|
661
|
+
if (!b.sql.casWon(deniedUpd).won) {
|
|
662
662
|
var racedDeny = await _getClaimRow(claimId);
|
|
663
663
|
throw new TypeError("shippingInsurance.markClaimDenied: claim " + claimId +
|
|
664
664
|
" is " + (racedDeny ? racedDeny.status : "gone") +
|
package/lib/sms-dispatcher.js
CHANGED
|
@@ -880,7 +880,7 @@ function create(opts) {
|
|
|
880
880
|
"failed_at = ?2 WHERE id = ?3 AND status = 'queued'",
|
|
881
881
|
["no_provider_at_dispatch", now, row.id],
|
|
882
882
|
);
|
|
883
|
-
if (
|
|
883
|
+
if (!b.sql.casWon(lostFail).won) continue;
|
|
884
884
|
advanced.push(await _getMessage(row.id));
|
|
885
885
|
continue;
|
|
886
886
|
}
|
|
@@ -892,7 +892,7 @@ function create(opts) {
|
|
|
892
892
|
"sent_at = ?2 WHERE id = ?3 AND status = 'queued'",
|
|
893
893
|
[providerSlug, now, row.id],
|
|
894
894
|
);
|
|
895
|
-
if (
|
|
895
|
+
if (!b.sql.casWon(claimed).won) continue;
|
|
896
896
|
advanced.push(await _getMessage(row.id));
|
|
897
897
|
}
|
|
898
898
|
return advanced;
|
package/lib/split-shipments.js
CHANGED
|
@@ -616,7 +616,7 @@ function create(opts) {
|
|
|
616
616
|
"WHERE id = ?2 AND status = 'proposed'",
|
|
617
617
|
[ts, input.plan.id],
|
|
618
618
|
);
|
|
619
|
-
if (
|
|
619
|
+
if (!b.sql.casWon(claim).won) {
|
|
620
620
|
// Lost the race (or the row was concurrently cancelled/executed
|
|
621
621
|
// between the read above and the claim). Re-read so the message
|
|
622
622
|
// reflects the row's actual current status.
|
package/lib/stock-alerts.js
CHANGED
|
@@ -623,7 +623,7 @@ function create(opts) {
|
|
|
623
623
|
"WHERE id = ?3 AND notified_at IS NULL",
|
|
624
624
|
[now, _hashUnsubToken(rowUnsubToken), row.id],
|
|
625
625
|
);
|
|
626
|
-
if (!claim ||
|
|
626
|
+
if (!claim || !b.sql.casWon(claim).won) continue;
|
|
627
627
|
row.notified_at = now;
|
|
628
628
|
row.unsubscribe_token = rowUnsubToken;
|
|
629
629
|
fired.push(row);
|
package/lib/stock-transfers.js
CHANGED
|
@@ -497,7 +497,7 @@ function create(opts) {
|
|
|
497
497
|
"carrier = ?2, tracking_number = ?3 WHERE id = ?4 AND status = 'open'",
|
|
498
498
|
[shippedAt, carrier, tracking, id],
|
|
499
499
|
);
|
|
500
|
-
if (
|
|
500
|
+
if (!b.sql.casWon(claim).won) {
|
|
501
501
|
throw new TypeError("stock-transfers.markShipped: transfer " + id +
|
|
502
502
|
" is no longer open (shipped by a concurrent call)");
|
|
503
503
|
}
|
|
@@ -602,7 +602,7 @@ function create(opts) {
|
|
|
602
602
|
"WHERE id = ?2 AND status IN ('shipped', 'in_transit')",
|
|
603
603
|
[receivedAt, id],
|
|
604
604
|
);
|
|
605
|
-
if (
|
|
605
|
+
if (!b.sql.casWon(claim).won) {
|
|
606
606
|
throw new TypeError("stock-transfers.markReceived: transfer " + id +
|
|
607
607
|
" is no longer shipped or in_transit (received by a concurrent call)");
|
|
608
608
|
}
|
|
@@ -667,7 +667,7 @@ function create(opts) {
|
|
|
667
667
|
"WHERE id = ?2 AND status = 'received'",
|
|
668
668
|
[ts, id],
|
|
669
669
|
);
|
|
670
|
-
if (
|
|
670
|
+
if (!b.sql.casWon(claim).won) {
|
|
671
671
|
throw new TypeError("stock-transfers.reconcile: transfer " + id +
|
|
672
672
|
" is no longer received (already reconciled by a concurrent call) — refusing to double-credit");
|
|
673
673
|
}
|
|
@@ -756,7 +756,7 @@ function create(opts) {
|
|
|
756
756
|
"WHERE id = ?2 AND status IN ('open', 'shipped', 'in_transit', 'received')",
|
|
757
757
|
[reason, id],
|
|
758
758
|
);
|
|
759
|
-
if (
|
|
759
|
+
if (!b.sql.casWon(claim).won) {
|
|
760
760
|
throw new TypeError("stock-transfers.markException: transfer " + id +
|
|
761
761
|
" is no longer in a non-terminal state (settled by a concurrent call)");
|
|
762
762
|
}
|
|
@@ -476,7 +476,7 @@ function create(opts) {
|
|
|
476
476
|
"WHERE id = ?2 AND status IN ('pending','delivered')",
|
|
477
477
|
[now, existing.id],
|
|
478
478
|
);
|
|
479
|
-
if (
|
|
479
|
+
if (!b.sql.casWon(claim).won) {
|
|
480
480
|
// Lost the race — re-read so we surface the actual post-race status.
|
|
481
481
|
var after = await _getRaw(existing.id);
|
|
482
482
|
var raceErr = new Error(
|
|
@@ -557,7 +557,7 @@ function create(opts) {
|
|
|
557
557
|
"WHERE id = ?3 AND status IN ('pending','delivered')",
|
|
558
558
|
[ts, reason, giftId],
|
|
559
559
|
);
|
|
560
|
-
if (
|
|
560
|
+
if (!b.sql.casWon(r).won) {
|
|
561
561
|
var after = await _getRaw(giftId);
|
|
562
562
|
var raceErr = new Error(
|
|
563
563
|
"subscriptionGifts.cancelGift: gift status changed to '" +
|
package/lib/suggestion-box.js
CHANGED
|
@@ -602,7 +602,7 @@ function create(opts) {
|
|
|
602
602
|
"VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
603
603
|
[voteId, sid, sessionHash, vote, ts],
|
|
604
604
|
);
|
|
605
|
-
var changed = ins && (
|
|
605
|
+
var changed = ins && b.sql.casWon(ins).won;
|
|
606
606
|
if (!changed) {
|
|
607
607
|
// Existing vote — surface "already voted" so the client can
|
|
608
608
|
// render a UI confirmation rather than silently no-op.
|
|
@@ -768,7 +768,7 @@ function create(opts) {
|
|
|
768
768
|
"WHERE id = ?3 AND status <> 'duplicate' AND archived_at IS NULL",
|
|
769
769
|
[cid, ts, sid],
|
|
770
770
|
);
|
|
771
|
-
if (
|
|
771
|
+
if (!b.sql.casWon(claim).won) {
|
|
772
772
|
// Lost the race (a concurrent linkDuplicates already flipped the
|
|
773
773
|
// source) — or it was archived between the read and the claim.
|
|
774
774
|
// Re-read to distinguish: a row now in 'duplicate' is the
|
package/lib/webhooks.js
CHANGED
|
@@ -589,9 +589,16 @@ function create(opts) {
|
|
|
589
589
|
"WHERE id = ?2 AND next_retry_at IS NOT NULL AND next_retry_at <= ?3 AND delivered_at IS NULL",
|
|
590
590
|
[now + RETRY_CLAIM_LEASE_MS, row.id, now],
|
|
591
591
|
);
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
592
|
+
// casWon inspects only top-level count fields (rowCount / changes /
|
|
593
|
+
// affectedRows). This module accepts an injected opts.query that may be
|
|
594
|
+
// a raw-D1 runner reporting the count under a nested meta.changes, so
|
|
595
|
+
// surface that as a top-level rowCount before the verdict — otherwise
|
|
596
|
+
// a successful claim with only meta.changes would be read as
|
|
597
|
+
// indeterminate and abort the tick, leaving the delivery merely leased.
|
|
598
|
+
if (claim && claim.rowCount == null && claim.meta && typeof claim.meta.changes === "number") {
|
|
599
|
+
claim.rowCount = claim.meta.changes;
|
|
600
|
+
}
|
|
601
|
+
if (!b.sql.casWon(claim).won) continue; // lost the claim to a concurrent tick
|
|
595
602
|
var d = await _attempt(row.id, endpoint, row.event_type, row.payload_json);
|
|
596
603
|
attempted.push(d);
|
|
597
604
|
}
|
package/lib/wishlist-alerts.js
CHANGED
|
@@ -650,7 +650,7 @@ function create(opts) {
|
|
|
650
650
|
+ ")",
|
|
651
651
|
[id, customerId, policySlug, sku, now, dedupeSince, capActive, weekSince, capValue],
|
|
652
652
|
);
|
|
653
|
-
return
|
|
653
|
+
return b.sql.casWon(r).won;
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
// Walk every wishlist entry that has a non-null variant_id. Whole-
|
package/lib/wishlist-digest.js
CHANGED
|
@@ -929,7 +929,7 @@ function create(opts) {
|
|
|
929
929
|
"WHERE id = ?2 AND status = 'active' AND next_dispatch_at = ?3",
|
|
930
930
|
[nextDispatchAt, enrollment.id, observedNext],
|
|
931
931
|
);
|
|
932
|
-
if (
|
|
932
|
+
if (!b.sql.casWon(claim).won) {
|
|
933
933
|
// Another tick already claimed this period — don't double-send.
|
|
934
934
|
continue;
|
|
935
935
|
}
|
package/package.json
CHANGED