@blamejs/blamejs-shop 0.4.62 → 0.4.64
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 +15 -4
- package/lib/customer-impersonation.js +26 -3
- package/lib/customer-merge.js +109 -28
- package/lib/customer-surveys.js +20 -5
- package/lib/order-exchanges.js +49 -18
- package/lib/payment-retries.js +41 -10
- package/lib/pick-lists.js +60 -22
- package/lib/preorder.js +163 -40
- package/lib/print-queue.js +18 -2
- package/lib/purchase-orders.js +131 -36
- package/lib/reorder-reminders.js +11 -7
- package/lib/seller-signup.js +29 -10
- package/lib/split-shipments.js +57 -16
- package/lib/suggestion-box.js +35 -2
- package/lib/wishlist-digest.js +10 -1
- package/package.json +1 -1
package/lib/split-shipments.js
CHANGED
|
@@ -603,26 +603,67 @@ function create(opts) {
|
|
|
603
603
|
}
|
|
604
604
|
|
|
605
605
|
var hydrated = _hydratePlan(row);
|
|
606
|
+
var ts = _now();
|
|
607
|
+
// Atomic claim BEFORE any shipment is created. The JS status
|
|
608
|
+
// check above is racy: two concurrent callers both read a
|
|
609
|
+
// `proposed` row and both fall through to createShipment,
|
|
610
|
+
// double-creating shipments. The conditional UPDATE is the real
|
|
611
|
+
// gate — SQLite serializes writers, so exactly one caller flips
|
|
612
|
+
// proposed → executed and gets rowCount === 1; the loser sees 0
|
|
613
|
+
// and refuses without touching orderTracking.
|
|
614
|
+
var claim = await query(
|
|
615
|
+
"UPDATE split_shipment_plans SET status = 'executed', executed_at = ?1 " +
|
|
616
|
+
"WHERE id = ?2 AND status = 'proposed'",
|
|
617
|
+
[ts, input.plan.id],
|
|
618
|
+
);
|
|
619
|
+
if (Number(claim.rowCount || 0) !== 1) {
|
|
620
|
+
// Lost the race (or the row was concurrently cancelled/executed
|
|
621
|
+
// between the read above and the claim). Re-read so the message
|
|
622
|
+
// reflects the row's actual current status.
|
|
623
|
+
var lost = await _getPlanRow(input.plan.id);
|
|
624
|
+
throw new TypeError("split-shipments.executeSplit: plan " + input.plan.id +
|
|
625
|
+
" is " + ((lost && lost.status) || "gone") +
|
|
626
|
+
", only proposed plans can be executed");
|
|
627
|
+
}
|
|
628
|
+
|
|
606
629
|
var shipmentIds = [];
|
|
607
|
-
|
|
608
|
-
var
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
630
|
+
try {
|
|
631
|
+
for (var i = 0; i < hydrated.shipments.length; i += 1) {
|
|
632
|
+
var parcel = hydrated.shipments[i];
|
|
633
|
+
var notes = "split:" + parcel.rationale + " (" + (i + 1) + "/" + hydrated.shipments.length + ")";
|
|
634
|
+
var createInput = {
|
|
635
|
+
order_id: input.order_id,
|
|
636
|
+
carrier: carrier,
|
|
637
|
+
notes: notes,
|
|
638
|
+
};
|
|
639
|
+
if (carrier === "other") {
|
|
640
|
+
createInput.carrier_other_name = carrierOtherName;
|
|
641
|
+
}
|
|
642
|
+
var s = await orderTracking.createShipment(createInput);
|
|
643
|
+
shipmentIds.push(s.id);
|
|
617
644
|
}
|
|
618
|
-
|
|
619
|
-
|
|
645
|
+
} catch (e) {
|
|
646
|
+
// A createShipment throw after the claim would strand the plan
|
|
647
|
+
// in `executed` with a partial shipment set. Revert the claim
|
|
648
|
+
// (self-targeting WHERE so it only fires on the row WE claimed)
|
|
649
|
+
// and rethrow so the operator can retry once the underlying
|
|
650
|
+
// failure clears. Any shipment rows already written by earlier
|
|
651
|
+
// parcels are reconciled via the orderTracking primitive's own
|
|
652
|
+
// per-shipment flow — the plan returns to `proposed`.
|
|
653
|
+
await query(
|
|
654
|
+
"UPDATE split_shipment_plans SET status = 'proposed', executed_at = NULL " +
|
|
655
|
+
"WHERE id = ?1 AND status = 'executed'",
|
|
656
|
+
[input.plan.id],
|
|
657
|
+
);
|
|
658
|
+
throw e;
|
|
620
659
|
}
|
|
621
|
-
|
|
660
|
+
|
|
661
|
+
// Second UPDATE persists the shipment-id list now that every
|
|
662
|
+
// parcel landed. Self-targeting WHERE id keyed off the row we
|
|
663
|
+
// already claimed — the status gate was the first UPDATE.
|
|
622
664
|
await query(
|
|
623
|
-
"UPDATE split_shipment_plans SET
|
|
624
|
-
|
|
625
|
-
[JSON.stringify(shipmentIds), ts, input.plan.id],
|
|
665
|
+
"UPDATE split_shipment_plans SET executed_shipment_ids_json = ?1 WHERE id = ?2",
|
|
666
|
+
[JSON.stringify(shipmentIds), input.plan.id],
|
|
626
667
|
);
|
|
627
668
|
return _hydratePlan(await _getPlanRow(input.plan.id));
|
|
628
669
|
},
|
package/lib/suggestion-box.js
CHANGED
|
@@ -754,11 +754,44 @@ function create(opts) {
|
|
|
754
754
|
var ts = _now();
|
|
755
755
|
var srcVotes = Number(src.vote_count) || 0;
|
|
756
756
|
|
|
757
|
-
|
|
757
|
+
// Atomic claim: flip the source to 'duplicate' under a WHERE that
|
|
758
|
+
// re-asserts the JS preconditions (not already a duplicate, not
|
|
759
|
+
// archived). SQLite/D1 serializes writers, so this conditional
|
|
760
|
+
// UPDATE is the exactly-once gate — two concurrent linkers both
|
|
761
|
+
// pass the read-time checks above, but only one wins this claim.
|
|
762
|
+
// The loser (rowCount === 0) did NOT transition the row, so it
|
|
763
|
+
// must NOT migrate votes a second time (which would double-count
|
|
764
|
+
// srcVotes onto the canonical); it returns the already-linked row.
|
|
765
|
+
var claim = await query(
|
|
758
766
|
"UPDATE suggestions SET status = 'duplicate', canonical_id = ?1, " +
|
|
759
|
-
"vote_count = 0, updated_at = ?2
|
|
767
|
+
"vote_count = 0, updated_at = ?2 " +
|
|
768
|
+
"WHERE id = ?3 AND status <> 'duplicate' AND archived_at IS NULL",
|
|
760
769
|
[cid, ts, sid],
|
|
761
770
|
);
|
|
771
|
+
if (Number(claim.rowCount || 0) !== 1) {
|
|
772
|
+
// Lost the race (a concurrent linkDuplicates already flipped the
|
|
773
|
+
// source) — or it was archived between the read and the claim.
|
|
774
|
+
// Re-read to distinguish: a row now in 'duplicate' is the
|
|
775
|
+
// expected concurrent-link outcome, so return it idempotently;
|
|
776
|
+
// anything else means the precondition genuinely no longer holds.
|
|
777
|
+
var current = await _getRaw(sid);
|
|
778
|
+
if (current && current.status === "duplicate") {
|
|
779
|
+
return {
|
|
780
|
+
suggestion_id: sid,
|
|
781
|
+
canonical_id: current.canonical_id,
|
|
782
|
+
migrated_votes: 0,
|
|
783
|
+
source: _decode(current),
|
|
784
|
+
canonical: _decode(await _getRaw(current.canonical_id)),
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
var lost = new Error("suggestionBox.linkDuplicates: source could not be claimed for linking");
|
|
788
|
+
lost.code = "SUGGESTION_ALREADY_DUPLICATE";
|
|
789
|
+
throw lost;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Only the claim winner reaches here — migrate the source's net
|
|
793
|
+
// vote_count (captured from the row this claim transitioned) onto
|
|
794
|
+
// the canonical exactly once.
|
|
762
795
|
if (srcVotes !== 0) {
|
|
763
796
|
await query(
|
|
764
797
|
"UPDATE suggestions SET vote_count = vote_count + ?1, updated_at = ?2 WHERE id = ?3",
|
package/lib/wishlist-digest.js
CHANGED
|
@@ -941,10 +941,19 @@ function create(opts) {
|
|
|
941
941
|
// the cursor back to observedNext, guarded on the value we claimed
|
|
942
942
|
// so a concurrent winner's advance is never clobbered.
|
|
943
943
|
var _unclaim = async function () {
|
|
944
|
+
// Roll the cursor to a value DISTINCT from the one this tick
|
|
945
|
+
// observed (observedNext + 1ms), NOT back to the exact observed
|
|
946
|
+
// value: a concurrent overlapping tick that read the same cursor
|
|
947
|
+
// is waiting to claim `next_dispatch_at = observedNext`, and
|
|
948
|
+
// restoring the exact value would let it match and re-send the
|
|
949
|
+
// digest in the same overlap. observedNext+1 stays due, so a LATER
|
|
950
|
+
// tick re-attempts (deferred, not re-fired in this overlap); the
|
|
951
|
+
// guard on the claimed value means a concurrent winner's advance
|
|
952
|
+
// is never clobbered.
|
|
944
953
|
await query(
|
|
945
954
|
"UPDATE wishlist_digest_enrollments SET next_dispatch_at = ?1 " +
|
|
946
955
|
"WHERE id = ?2 AND next_dispatch_at = ?3",
|
|
947
|
-
[observedNext, enrollment.id, nextDispatchAt],
|
|
956
|
+
[observedNext + 1, enrollment.id, nextDispatchAt],
|
|
948
957
|
);
|
|
949
958
|
};
|
|
950
959
|
|
package/package.json
CHANGED