@blamejs/blamejs-shop 0.4.28 → 0.4.29
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/README.md +1 -1
- package/SECURITY.md +9 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/auto-discount.js +177 -6
- package/lib/checkout.js +300 -129
- package/lib/gift-card-ledger.js +208 -88
- package/lib/giftcards.js +56 -0
- package/lib/loyalty.js +61 -1
- package/lib/payment.js +33 -2
- package/lib/store-credit.js +99 -79
- package/lib/storefront.js +2 -2
- package/package.json +1 -1
package/lib/checkout.js
CHANGED
|
@@ -606,35 +606,60 @@ function create(deps) {
|
|
|
606
606
|
// card; a card worth less reduces the amount due, never below 0.
|
|
607
607
|
var grand = quote.totals.grand_total_minor;
|
|
608
608
|
var applied = card.balance_minor < grand ? card.balance_minor : grand;
|
|
609
|
+
// A zero balance applies nothing — treat it as no credit rather than
|
|
610
|
+
// attempting a zero-amount debit (an anomalous active card at 0 would
|
|
611
|
+
// otherwise turn into a validation throw deep in the redeem).
|
|
612
|
+
if (applied <= 0) return null;
|
|
609
613
|
// `code_plain` is the bearer code the redeem decrement re-hashes;
|
|
610
614
|
// it lives only in this in-memory resolution, never persisted.
|
|
611
615
|
return { card: card, code_plain: code, applied_minor: applied, balance_minor: card.balance_minor };
|
|
612
616
|
}
|
|
613
617
|
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
// or a ledger view that lags the card row) must not throw out of the
|
|
628
|
-
// post-create redeem path and strand the already-created order + its live
|
|
629
|
-
// PaymentIntent. The card-row debit already happened; the ledger row is
|
|
630
|
-
// reconcilable from the redemption record. Same posture the admin issue
|
|
631
|
-
// route takes when seeding the opening credit.
|
|
632
|
-
async function _redeemGiftCard(resolved, orderId) {
|
|
633
|
-
var redemption = await giftcards.redeem({
|
|
618
|
+
// Debit the resolved gift-card credit — the authoritative double-spend
|
|
619
|
+
// gate. Runs BEFORE the order (and any PaymentIntent / PayPal order)
|
|
620
|
+
// exists: two checkouts presenting the same card race the
|
|
621
|
+
// `balance_minor >= ?` SQL predicate HERE, while nothing has been
|
|
622
|
+
// charged or created, so the loser's coded GIFTCARD_INSUFFICIENT_BALANCE
|
|
623
|
+
// propagates into a clean rollback (holds + claim released) and a
|
|
624
|
+
// re-quote — never a second paid order against an already-spent card.
|
|
625
|
+
// The redemption row is written with a NULL order id and attached to the
|
|
626
|
+
// order by _settleGiftCardPostCreate once the order row exists; a
|
|
627
|
+
// checkout that dies between debit and order creation reverses the debit
|
|
628
|
+
// via giftcards.reverseRedemptionById in the rollback path.
|
|
629
|
+
async function _debitGiftCard(resolved) {
|
|
630
|
+
return giftcards.redeem({
|
|
634
631
|
code: resolved.code_plain,
|
|
635
|
-
order_id:
|
|
632
|
+
order_id: null,
|
|
636
633
|
amount_minor: resolved.applied_minor,
|
|
637
634
|
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Attach a pre-charge gift-card debit to the order it paid for and write
|
|
638
|
+
// the operator-facing ledger audit row. The order link is what the
|
|
639
|
+
// refund/cancel reversers key on (reverseRedemption /
|
|
640
|
+
// reverseRedemptionProRata select by order_id), so a link failure is
|
|
641
|
+
// surfaced LOUDLY to the audit sink for reconciliation — but neither the
|
|
642
|
+
// link nor the ledger row may throw out of the post-create path: the
|
|
643
|
+
// order and its live charge already exist, the card debit already
|
|
644
|
+
// landed, and both writes are reconcilable from the redemption record.
|
|
645
|
+
async function _settleGiftCardPostCreate(resolved, redemption, orderId) {
|
|
646
|
+
try {
|
|
647
|
+
var linked = await giftcards.linkRedemptionToOrder(redemption.redemption_id, orderId);
|
|
648
|
+
if (!linked) throw new Error("redemption " + redemption.redemption_id + " did not accept the order link");
|
|
649
|
+
} catch (linkErr) {
|
|
650
|
+
try {
|
|
651
|
+
b.audit.safeEmit({
|
|
652
|
+
action: "checkout.giftcard.order_link.error",
|
|
653
|
+
outcome: "failure",
|
|
654
|
+
metadata: {
|
|
655
|
+
gift_card_id: resolved.card.id,
|
|
656
|
+
redemption_id: redemption.redemption_id,
|
|
657
|
+
order_id: orderId,
|
|
658
|
+
message: (linkErr && linkErr.message) || String(linkErr),
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
} catch (_auditErr) { /* drop-silent — the redemption record is the durable trail */ }
|
|
662
|
+
}
|
|
638
663
|
if (giftCardLedger) {
|
|
639
664
|
try {
|
|
640
665
|
await giftCardLedger.debit({
|
|
@@ -659,32 +684,51 @@ function create(deps) {
|
|
|
659
684
|
} catch (_auditErr) { /* drop-silent — the redemption record is the durable trail */ }
|
|
660
685
|
}
|
|
661
686
|
}
|
|
662
|
-
return redemption;
|
|
663
687
|
}
|
|
664
688
|
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
try {
|
|
678
|
-
return await fn();
|
|
679
|
-
} catch (e) {
|
|
689
|
+
// Reverse the pre-charge credit debits when a checkout dies BEFORE its
|
|
690
|
+
// order exists (PaymentIntent refused, PayPal open failed, order insert
|
|
691
|
+
// threw). The card/points were taken for an order that will now never
|
|
692
|
+
// be created, so the rollback paths call this before releasing holds and
|
|
693
|
+
// the cart claim. Both reversals are claim-guarded (exactly-once) and
|
|
694
|
+
// keyed on the debit row itself — no order id was ever attached. A
|
|
695
|
+
// reversal failure is audited, never thrown: the original checkout error
|
|
696
|
+
// is the actionable one, and the unreversed debit is reconcilable from
|
|
697
|
+
// the audit trail.
|
|
698
|
+
async function _reverseCreditDebits(rollbackCtx) {
|
|
699
|
+
if (!rollbackCtx) return;
|
|
700
|
+
if (rollbackCtx.giftCardDebit) {
|
|
680
701
|
try {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
702
|
+
await giftcards.reverseRedemptionById(rollbackCtx.giftCardDebit.redemption.redemption_id);
|
|
703
|
+
} catch (revErr) {
|
|
704
|
+
try {
|
|
705
|
+
b.audit.safeEmit({
|
|
706
|
+
action: "checkout.giftcard.rollback_reverse.error",
|
|
707
|
+
outcome: "failure",
|
|
708
|
+
metadata: {
|
|
709
|
+
redemption_id: rollbackCtx.giftCardDebit.redemption.redemption_id,
|
|
710
|
+
amount_minor: rollbackCtx.giftCardDebit.resolved.applied_minor,
|
|
711
|
+
message: (revErr && revErr.message) || String(revErr),
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
} catch (_auditErr) { /* drop-silent — the original checkout error is the actionable one */ }
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (rollbackCtx.loyaltyDebit) {
|
|
718
|
+
try {
|
|
719
|
+
await loyalty.reverseRedemptionById(rollbackCtx.loyaltyDebit.redemption.tx_id);
|
|
720
|
+
} catch (revErr) {
|
|
721
|
+
try {
|
|
722
|
+
b.audit.safeEmit({
|
|
723
|
+
action: "checkout.loyalty.rollback_reverse.error",
|
|
724
|
+
outcome: "failure",
|
|
725
|
+
metadata: {
|
|
726
|
+
tx_id: rollbackCtx.loyaltyDebit.redemption.tx_id,
|
|
727
|
+
message: (revErr && revErr.message) || String(revErr),
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
} catch (_auditErr) { /* drop-silent — the original checkout error is the actionable one */ }
|
|
731
|
+
}
|
|
688
732
|
}
|
|
689
733
|
}
|
|
690
734
|
|
|
@@ -748,45 +792,127 @@ function create(deps) {
|
|
|
748
792
|
return { points: spentPoints, applied_minor: appliedMinor };
|
|
749
793
|
}
|
|
750
794
|
|
|
751
|
-
//
|
|
752
|
-
// `
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
async function
|
|
759
|
-
|
|
795
|
+
// Debit the resolved loyalty credit — same pre-charge discipline as the
|
|
796
|
+
// gift-card debit: the `balance_points >= ?` predicate in loyalty.redeem
|
|
797
|
+
// is the cross-cart double-spend gate, so it must land before any money
|
|
798
|
+
// moves. The burn's ledger row is written with a NULL order id and
|
|
799
|
+
// attached by _settleLoyaltyPostCreate once the order exists; a checkout
|
|
800
|
+
// that dies in between reverses via loyalty.reverseRedemptionById in the
|
|
801
|
+
// rollback path.
|
|
802
|
+
async function _debitLoyalty(resolved, customerId) {
|
|
803
|
+
return loyalty.redeem({
|
|
760
804
|
customer_id: customerId,
|
|
761
805
|
points: resolved.points,
|
|
762
|
-
order_id:
|
|
763
|
-
notes: "checkout-credit
|
|
806
|
+
order_id: null,
|
|
807
|
+
notes: "checkout-credit",
|
|
764
808
|
});
|
|
765
809
|
}
|
|
766
810
|
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
811
|
+
// Attach a pre-charge loyalty burn to the order it tendered for. The
|
|
812
|
+
// order link is what restoreRedemption keys on for refund restores, so a
|
|
813
|
+
// link failure is audited loudly — but never thrown: the order and its
|
|
814
|
+
// live charge already exist and the burn is reconcilable from the
|
|
815
|
+
// ledger row.
|
|
816
|
+
async function _settleLoyaltyPostCreate(redemption, orderId) {
|
|
817
|
+
try {
|
|
818
|
+
var linked = await loyalty.linkRedemptionToOrder(redemption.tx_id, orderId);
|
|
819
|
+
if (!linked) throw new Error("loyalty tx " + redemption.tx_id + " did not accept the order link");
|
|
820
|
+
} catch (linkErr) {
|
|
821
|
+
try {
|
|
822
|
+
b.audit.safeEmit({
|
|
823
|
+
action: "checkout.loyalty.order_link.error",
|
|
824
|
+
outcome: "failure",
|
|
825
|
+
metadata: {
|
|
826
|
+
tx_id: redemption.tx_id,
|
|
827
|
+
order_id: orderId,
|
|
828
|
+
message: (linkErr && linkErr.message) || String(linkErr),
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
} catch (_auditErr) { /* drop-silent — the burn row is the durable trail */ }
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Reserve each applied auto-discount rule's redemption BEFORE any money
|
|
836
|
+
// moves — a capped rule is a finite resource, so checkout claims it
|
|
837
|
+
// pre-charge under the same reserve-then-settle discipline inventory
|
|
838
|
+
// holds follow. A refused claim (cap exhausted, or lost to a concurrent
|
|
839
|
+
// order — the bypass that let a single-use code apply to every racing
|
|
840
|
+
// checkout) FAILS CLOSED with a coded error: the buyer re-quotes and
|
|
841
|
+
// sees the true price, never a silently-different charge. Claims are
|
|
842
|
+
// stashed on the rollback holder so a later pre-order throw releases
|
|
843
|
+
// them; claim-ref reuse makes a same-checkout retry idempotent.
|
|
844
|
+
async function _claimAutoDiscounts(quote, customerId, claimRef, rollbackHolder) {
|
|
845
|
+
if (!autoDiscount || typeof autoDiscount.claimRedemption !== "function") return;
|
|
846
|
+
var applied = quote && Array.isArray(quote.auto_discounts) ? quote.auto_discounts : [];
|
|
847
|
+
for (var i = 0; i < applied.length; i += 1) {
|
|
848
|
+
var a = applied[i];
|
|
849
|
+
if (!a || !a.rule_slug) continue;
|
|
850
|
+
var claim = await autoDiscount.claimRedemption({
|
|
851
|
+
rule_slug: a.rule_slug,
|
|
852
|
+
claim_ref: claimRef,
|
|
853
|
+
savings_minor: a.savings_minor,
|
|
854
|
+
customer_id: customerId || undefined,
|
|
855
|
+
});
|
|
856
|
+
if (!claim.claimed) {
|
|
857
|
+
var gone = new Error("checkout: the " + JSON.stringify(a.rule_slug) +
|
|
858
|
+
" discount is no longer available (" + claim.reason + ")");
|
|
859
|
+
gone.code = "AUTO_DISCOUNT_EXHAUSTED";
|
|
860
|
+
throw gone;
|
|
861
|
+
}
|
|
862
|
+
if (rollbackHolder) {
|
|
863
|
+
if (!rollbackHolder.autoDiscountClaims) rollbackHolder.autoDiscountClaims = [];
|
|
864
|
+
rollbackHolder.autoDiscountClaims.push({ rule_slug: a.rule_slug, claim_ref: claimRef });
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Re-key the pre-charge claims to the order that now exists, so the
|
|
870
|
+
// redemption rows the metrics/admin surfaces read carry the real order
|
|
871
|
+
// id. POST-COMMIT best-effort with a loud audit — the order and its
|
|
872
|
+
// charge already exist; the claim row itself (and the cap it reserved)
|
|
873
|
+
// landed pre-charge and is correct regardless.
|
|
874
|
+
async function _linkAutoDiscounts(quote, claimRef, orderId) {
|
|
875
|
+
if (!autoDiscount || typeof autoDiscount.linkClaimToOrder !== "function") return;
|
|
776
876
|
var applied = quote && Array.isArray(quote.auto_discounts) ? quote.auto_discounts : [];
|
|
777
877
|
for (var i = 0; i < applied.length; i += 1) {
|
|
778
878
|
var a = applied[i];
|
|
779
879
|
if (!a || !a.rule_slug) continue;
|
|
780
880
|
try {
|
|
781
|
-
await autoDiscount.
|
|
782
|
-
rule_slug:
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
customer_id: customerId || undefined,
|
|
881
|
+
await autoDiscount.linkClaimToOrder({
|
|
882
|
+
rule_slug: a.rule_slug,
|
|
883
|
+
claim_ref: claimRef,
|
|
884
|
+
order_id: orderId,
|
|
786
885
|
});
|
|
787
|
-
} catch (
|
|
788
|
-
|
|
789
|
-
|
|
886
|
+
} catch (linkErr) {
|
|
887
|
+
try {
|
|
888
|
+
b.audit.safeEmit({
|
|
889
|
+
action: "checkout.autodiscount.order_link.error",
|
|
890
|
+
outcome: "failure",
|
|
891
|
+
metadata: { rule_slug: a.rule_slug, order_id: orderId, message: (linkErr && linkErr.message) || String(linkErr) },
|
|
892
|
+
});
|
|
893
|
+
} catch (_auditErr) { /* drop-silent — the claim row is the durable trail */ }
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Release pre-charge discount claims when the checkout dies before its
|
|
899
|
+
// order exists — the reservation goes back to the cap. Audited, never
|
|
900
|
+
// thrown: the original checkout error is the actionable one.
|
|
901
|
+
async function _releaseAutoDiscountClaims(rollbackHolder) {
|
|
902
|
+
if (!rollbackHolder || !rollbackHolder.autoDiscountClaims) return;
|
|
903
|
+
if (!autoDiscount || typeof autoDiscount.releaseClaim !== "function") return;
|
|
904
|
+
for (var i = 0; i < rollbackHolder.autoDiscountClaims.length; i += 1) {
|
|
905
|
+
var cl = rollbackHolder.autoDiscountClaims[i];
|
|
906
|
+
try {
|
|
907
|
+
await autoDiscount.releaseClaim({ rule_slug: cl.rule_slug, claim_ref: cl.claim_ref });
|
|
908
|
+
} catch (relErr) {
|
|
909
|
+
try {
|
|
910
|
+
b.audit.safeEmit({
|
|
911
|
+
action: "checkout.autodiscount.claim_release.error",
|
|
912
|
+
outcome: "failure",
|
|
913
|
+
metadata: { rule_slug: cl.rule_slug, claim_ref: cl.claim_ref, message: (relErr && relErr.message) || String(relErr) },
|
|
914
|
+
});
|
|
915
|
+
} catch (_auditErr) { /* drop-silent — the original checkout error is the actionable one */ }
|
|
790
916
|
}
|
|
791
917
|
}
|
|
792
918
|
}
|
|
@@ -1155,6 +1281,12 @@ function create(deps) {
|
|
|
1155
1281
|
// retry on the same cart. Both releases are atomic + self-targeting,
|
|
1156
1282
|
// so neither can disturb a genuinely completed checkout.
|
|
1157
1283
|
if (!rollbackCtx.orderCreated) {
|
|
1284
|
+
// Reverse any pre-charge credit debits and discount claims first
|
|
1285
|
+
// — the card/points/caps were taken for an order that will now
|
|
1286
|
+
// never exist (both helpers are claim-guarded, audited, and
|
|
1287
|
+
// never throw).
|
|
1288
|
+
await _reverseCreditDebits(rollbackCtx);
|
|
1289
|
+
await _releaseAutoDiscountClaims(rollbackCtx);
|
|
1158
1290
|
if (rollbackCtx.stockHolds) {
|
|
1159
1291
|
try { await _releaseStockHolds(rollbackCtx.stockHolds); }
|
|
1160
1292
|
catch (_holdErr) { /* drop-silent — claim release below + original error are the actionable parts */ }
|
|
@@ -1213,6 +1345,32 @@ function create(deps) {
|
|
|
1213
1345
|
}
|
|
1214
1346
|
var amountDue = afterGiftCard - (loy ? loy.applied_minor : 0);
|
|
1215
1347
|
|
|
1348
|
+
// Reserve the applied auto-discount redemptions FIRST (cheapest
|
|
1349
|
+
// claim, fails closed on an exhausted cap with a clean re-quote),
|
|
1350
|
+
// then the credit debits — all inside the rollback-protected region.
|
|
1351
|
+
await _claimAutoDiscounts(quote, loyaltyCustomerId, idempotencyKey, rollbackCtx);
|
|
1352
|
+
|
|
1353
|
+
// Authoritative credit debits — PRE-charge, inside the rollback-
|
|
1354
|
+
// protected region (rollbackCtx.orderCreated is still false). The SQL
|
|
1355
|
+
// balance gates in giftcards.redeem / loyalty.redeem are the
|
|
1356
|
+
// CROSS-CART double-spend guard: two checkouts presenting the same
|
|
1357
|
+
// card (or the same points balance) race those predicates here,
|
|
1358
|
+
// before any PaymentIntent or order exists, and the loser's coded
|
|
1359
|
+
// error propagates into confirm()'s clean rollback (holds + claim
|
|
1360
|
+
// released) for a re-quote. The debited rows carry no order id yet —
|
|
1361
|
+
// they are linked post-create, or reversed by the rollback when a
|
|
1362
|
+
// later step throws before the order exists.
|
|
1363
|
+
var gcRedemption = null;
|
|
1364
|
+
if (gc) {
|
|
1365
|
+
gcRedemption = await _debitGiftCard(gc);
|
|
1366
|
+
if (rollbackCtx) rollbackCtx.giftCardDebit = { resolved: gc, redemption: gcRedemption };
|
|
1367
|
+
}
|
|
1368
|
+
var loyRedemption = null;
|
|
1369
|
+
if (loy) {
|
|
1370
|
+
loyRedemption = await _debitLoyalty(loy, loyaltyCustomerId);
|
|
1371
|
+
if (rollbackCtx) rollbackCtx.loyaltyDebit = { redemption: loyRedemption };
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1216
1374
|
var orderLines = quote.lines.map(function (l) {
|
|
1217
1375
|
return {
|
|
1218
1376
|
variant_id: l.variant_id,
|
|
@@ -1256,20 +1414,17 @@ function create(deps) {
|
|
|
1256
1414
|
// The pending order now owns the holds — a throw from here on must
|
|
1257
1415
|
// NOT blanket-release them (see the conditional rollback in confirm).
|
|
1258
1416
|
if (rollbackCtx) rollbackCtx.orderCreated = true;
|
|
1259
|
-
//
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
1262
|
-
//
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
// placed order. Runs post-commit so a recording failure can't
|
|
1271
|
-
// reverse an order that's already paid.
|
|
1272
|
-
await _recordAutoDiscounts(quote, paidOrder.id, cartRow.customer_id || null);
|
|
1417
|
+
// The credits were already debited pre-charge above (the debit IS
|
|
1418
|
+
// the double-spend gate); what remains is attaching the debit rows
|
|
1419
|
+
// to the order that now exists + the ledger audit row. Both are
|
|
1420
|
+
// best-effort with loud audits — a failure here must not strand
|
|
1421
|
+
// the cart un-converted.
|
|
1422
|
+
if (gc) await _settleGiftCardPostCreate(gc, gcRedemption, paidOrder.id);
|
|
1423
|
+
if (loy) await _settleLoyaltyPostCreate(loyRedemption, paidOrder.id);
|
|
1424
|
+
// Re-key the pre-charge discount claims to the placed order. Runs
|
|
1425
|
+
// post-commit so a link failure can't reverse an order that's
|
|
1426
|
+
// already paid (the cap reservation itself landed pre-charge).
|
|
1427
|
+
await _linkAutoDiscounts(quote, idempotencyKey, paidOrder.id);
|
|
1273
1428
|
// Best-effort: record how the cart-level discount split across the
|
|
1274
1429
|
// order lines (accounting + refund precision). Post-commit +
|
|
1275
1430
|
// drop-silent — no impact on the already-settled order.
|
|
@@ -1327,25 +1482,21 @@ function create(deps) {
|
|
|
1327
1482
|
// settles them now that the order exists.
|
|
1328
1483
|
if (rollbackCtx) rollbackCtx.orderCreated = true;
|
|
1329
1484
|
|
|
1330
|
-
//
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1333
|
-
//
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1336
|
-
// and
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
await
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
// Best-effort: record the auto-discount redemptions against the
|
|
1346
|
-
// placed order. Runs post-commit so a recording failure can't
|
|
1347
|
-
// reverse or fail an order whose PaymentIntent already exists.
|
|
1348
|
-
await _recordAutoDiscounts(quote, createdOrder.id, cartRow.customer_id || null);
|
|
1485
|
+
// Attach the pre-charge credit debits to the created order. The
|
|
1486
|
+
// authoritative card/points debits already landed BEFORE the
|
|
1487
|
+
// PaymentIntent was opened (the debit is the double-spend gate); the
|
|
1488
|
+
// remaining writes — the order link the refund reversers key on, and
|
|
1489
|
+
// the gift-card ledger audit row — are POST-COMMIT best-effort with
|
|
1490
|
+
// loud audits, because the order + its live PaymentIntent now exist
|
|
1491
|
+
// and a throw from here would leave the cart un-converted with an
|
|
1492
|
+
// orphaned charge (the conditional rollback above no longer fires —
|
|
1493
|
+
// orderCreated is set).
|
|
1494
|
+
if (gc) await _settleGiftCardPostCreate(gc, gcRedemption, createdOrder.id);
|
|
1495
|
+
if (loy) await _settleLoyaltyPostCreate(loyRedemption, createdOrder.id);
|
|
1496
|
+
// Re-key the pre-charge discount claims to the placed order. Runs
|
|
1497
|
+
// post-commit so a link failure can't reverse or fail an order whose
|
|
1498
|
+
// PaymentIntent already exists (the cap reservation landed pre-charge).
|
|
1499
|
+
await _linkAutoDiscounts(quote, idempotencyKey, createdOrder.id);
|
|
1349
1500
|
// Best-effort: record how the cart-level discount split across the
|
|
1350
1501
|
// order lines (accounting + refund precision). Post-commit +
|
|
1351
1502
|
// drop-silent — the PaymentIntent already exists; this never
|
|
@@ -1582,6 +1733,7 @@ function create(deps) {
|
|
|
1582
1733
|
// approves is reaped by the stale-pending reaper (no PayPal-side
|
|
1583
1734
|
// cancel needed — the capture path guards on local status).
|
|
1584
1735
|
var ppOrderCreated = false;
|
|
1736
|
+
var ppRollback = {};
|
|
1585
1737
|
try {
|
|
1586
1738
|
// Resolve an optional gift-card credit before opening the PayPal
|
|
1587
1739
|
// order so a bad code fails without a remote round-trip.
|
|
@@ -1602,6 +1754,26 @@ function create(deps) {
|
|
|
1602
1754
|
if (loy.applied_minor <= 0) loy = null;
|
|
1603
1755
|
}
|
|
1604
1756
|
var amountDue = afterGiftCard - (loy ? loy.applied_minor : 0);
|
|
1757
|
+
|
|
1758
|
+
// Reserve the applied auto-discount redemptions, then the credit
|
|
1759
|
+
// debits — PRE-charge, same cross-cart double-spend discipline as
|
|
1760
|
+
// the Stripe confirm (see _confirmAfterHolds): the SQL gates race
|
|
1761
|
+
// here, before the PayPal order or the local order exist, and the
|
|
1762
|
+
// loser's coded error rolls this checkout back cleanly. Linked
|
|
1763
|
+
// post-create; reversed by the catch below when a later step throws
|
|
1764
|
+
// before the order exists.
|
|
1765
|
+
await _claimAutoDiscounts(quote, ppLoyaltyCustomerId, idempotencyKey, ppRollback);
|
|
1766
|
+
var gcRedemption = null;
|
|
1767
|
+
if (gc) {
|
|
1768
|
+
gcRedemption = await _debitGiftCard(gc);
|
|
1769
|
+
ppRollback.giftCardDebit = { resolved: gc, redemption: gcRedemption };
|
|
1770
|
+
}
|
|
1771
|
+
var loyRedemption = null;
|
|
1772
|
+
if (loy) {
|
|
1773
|
+
loyRedemption = await _debitLoyalty(loy, ppLoyaltyCustomerId);
|
|
1774
|
+
ppRollback.loyaltyDebit = { redemption: loyRedemption };
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1605
1777
|
var emailHash = customers ? customers.hashEmail(email) : null;
|
|
1606
1778
|
var ppLines = quote.lines.map(function (l) {
|
|
1607
1779
|
return { variant_id: l.variant_id, sku: l.sku, qty: l.qty, unit_amount_minor: l.unit_amount_minor, unit_currency: l.unit_currency, stock_held_qty: l._held_qty || 0 };
|
|
@@ -1629,15 +1801,12 @@ function create(deps) {
|
|
|
1629
1801
|
lines: ppLines,
|
|
1630
1802
|
});
|
|
1631
1803
|
ppOrderCreated = true;
|
|
1632
|
-
// Same settlement posture as the Stripe branches: the
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
await
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
await _settleCreditPostCreate("loyalty", paidOrder.id, function () {
|
|
1639
|
-
return loy ? _redeemLoyalty(loy, ppLoyaltyCustomerId, paidOrder.id) : null;
|
|
1640
|
-
});
|
|
1804
|
+
// Same settlement posture as the Stripe branches: the authoritative
|
|
1805
|
+
// debits + claims landed pre-charge; attach them to the order that
|
|
1806
|
+
// now exists (best-effort, loudly audited — never stranding).
|
|
1807
|
+
if (gc) await _settleGiftCardPostCreate(gc, gcRedemption, paidOrder.id);
|
|
1808
|
+
if (loy) await _settleLoyaltyPostCreate(loyRedemption, paidOrder.id);
|
|
1809
|
+
await _linkAutoDiscounts(quote, idempotencyKey, paidOrder.id);
|
|
1641
1810
|
await cart.setStatus(quote.cart_id, "converted");
|
|
1642
1811
|
var settled = await order.transition(paidOrder.id, "mark_paid", {
|
|
1643
1812
|
reason: loy && !gc ? "loyalty:full" : "gift_card:full",
|
|
@@ -1673,18 +1842,14 @@ function create(deps) {
|
|
|
1673
1842
|
lines: ppLines,
|
|
1674
1843
|
});
|
|
1675
1844
|
ppOrderCreated = true;
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
//
|
|
1679
|
-
//
|
|
1680
|
-
//
|
|
1681
|
-
|
|
1682
|
-
await
|
|
1683
|
-
|
|
1684
|
-
});
|
|
1685
|
-
await _settleCreditPostCreate("loyalty", createdOrder.id, function () {
|
|
1686
|
-
return loy ? _redeemLoyalty(loy, ppLoyaltyCustomerId, createdOrder.id) : null;
|
|
1687
|
-
});
|
|
1845
|
+
// Attach the pre-charge debits + claims to the created order — the
|
|
1846
|
+
// authoritative card/points debits and cap reservations landed before
|
|
1847
|
+
// the PayPal order was opened; the order links + ledger row are
|
|
1848
|
+
// post-commit best-effort with loud audits (a throw here would leave
|
|
1849
|
+
// the cart active with an orphaned PayPal order).
|
|
1850
|
+
if (gc) await _settleGiftCardPostCreate(gc, gcRedemption, createdOrder.id);
|
|
1851
|
+
if (loy) await _settleLoyaltyPostCreate(loyRedemption, createdOrder.id);
|
|
1852
|
+
await _linkAutoDiscounts(quote, idempotencyKey, createdOrder.id);
|
|
1688
1853
|
await cart.setStatus(quote.cart_id, "converted");
|
|
1689
1854
|
return {
|
|
1690
1855
|
order: createdOrder, paypal_order_id: ppOrder.id, status: ppOrder.status,
|
|
@@ -1693,9 +1858,15 @@ function create(deps) {
|
|
|
1693
1858
|
};
|
|
1694
1859
|
} catch (e) {
|
|
1695
1860
|
// A throw BEFORE the order row commits (PayPal open failure,
|
|
1696
|
-
// gift-card error)
|
|
1697
|
-
//
|
|
1698
|
-
|
|
1861
|
+
// gift-card error) reverses any pre-charge credit debits and
|
|
1862
|
+
// discount claims and releases the holds so PayPal can't strand
|
|
1863
|
+
// stock, burn credit, or leak a cap reservation. After the order
|
|
1864
|
+
// commits it owns the holds — don't blanket-release.
|
|
1865
|
+
if (!ppOrderCreated) {
|
|
1866
|
+
await _reverseCreditDebits(ppRollback);
|
|
1867
|
+
await _releaseAutoDiscountClaims(ppRollback);
|
|
1868
|
+
await _releaseStockHolds(ppHolds);
|
|
1869
|
+
}
|
|
1699
1870
|
throw e;
|
|
1700
1871
|
}
|
|
1701
1872
|
},
|