@mohasinac/appkit 4.42.13 → 4.42.14
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.
|
@@ -19,7 +19,7 @@ import { sendOrderConfirmationEmail } from "../../../../features/contact/server"
|
|
|
19
19
|
import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
|
|
20
20
|
import { resolveCouponCategorySlugs } from "../../../../features/promotions/actions/coupon-actions";
|
|
21
21
|
import { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines";
|
|
22
|
-
import { activeLane, laneOf } from "../../../shared/checkout/lanes";
|
|
22
|
+
import { activeLane, defaultCheckoutItems, laneOf, } from "../../../shared/checkout/lanes";
|
|
23
23
|
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, computeCheckoutFees, allocateCheckoutFees, } from "../../../shared/fees/calculator";
|
|
24
24
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase";
|
|
25
25
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore";
|
|
@@ -655,8 +655,13 @@ export async function createCheckoutOrderAction(input) {
|
|
|
655
655
|
const selectedSet = cart.selectedItemIds?.length
|
|
656
656
|
? new Set(cart.selectedItemIds)
|
|
657
657
|
: null;
|
|
658
|
-
|
|
659
|
-
|
|
658
|
+
// No explicit selection means THE ACTIVE LANE, not the whole cart — see
|
|
659
|
+
// defaultCheckoutItems. Falling back to every line made assertCheckoutLane
|
|
660
|
+
// below throw on any multi-lane cart, which is most of them.
|
|
661
|
+
const candidateItems = selectedSet
|
|
662
|
+
? cart.items.filter((item) => selectedSet.has(item.itemId))
|
|
663
|
+
: defaultCheckoutItems(cart.items);
|
|
664
|
+
const cartItems = candidateItems.filter((item) => !excludedSet.has(item.productId));
|
|
660
665
|
if (cartItems.length === 0) {
|
|
661
666
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
662
667
|
}
|
|
@@ -1426,11 +1431,17 @@ export async function verifyAndPlacePhonePeOrderAction(input) {
|
|
|
1426
1431
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
1427
1432
|
}
|
|
1428
1433
|
// Same lane priority + locked-line revalidation the manual/COD path runs.
|
|
1429
|
-
//
|
|
1430
|
-
//
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
+
//
|
|
1435
|
+
// 🛑 This path carries no selectedItemIds, so it settles the ACTIVE LANE —
|
|
1436
|
+
// not the cart. It used to pass `cart.items` as its own "selected" set,
|
|
1437
|
+
// which made assertCheckoutLane compare the whole cart against its own
|
|
1438
|
+
// active lane and throw on every multi-lane cart. Everything downstream
|
|
1439
|
+
// reads `settleItems` for the same reason: stock decrements, jurisdiction
|
|
1440
|
+
// and the order rows must cover exactly what was charged for.
|
|
1441
|
+
const settleItems = defaultCheckoutItems(cart.items);
|
|
1442
|
+
assertCheckoutLane(cart.items, settleItems);
|
|
1443
|
+
await assertLockedLinesStillValid(settleItems, uid);
|
|
1444
|
+
const isDigitalCartPp = cartIsDigitalOnly(settleItems);
|
|
1434
1445
|
let shippingAddress;
|
|
1435
1446
|
let resolvedAddressPp = null;
|
|
1436
1447
|
if (!isDigitalCartPp) {
|
|
@@ -1451,7 +1462,7 @@ export async function verifyAndPlacePhonePeOrderAction(input) {
|
|
|
1451
1462
|
// cart line gets paired with a "representative" product (first member id
|
|
1452
1463
|
// for bundles, productId for regular items). The full bundle-member
|
|
1453
1464
|
// decrement runs against `expansionPaid.decrements` lower down.
|
|
1454
|
-
const expansionPaid = getExpandedDecrements(
|
|
1465
|
+
const expansionPaid = getExpandedDecrements(settleItems);
|
|
1455
1466
|
const productByIdPaid = new Map();
|
|
1456
1467
|
// Independent reads across distinct product docs — batch them instead of
|
|
1457
1468
|
// awaiting one findById per product (mirrors the COD/UPI path above, which
|
|
@@ -1462,7 +1473,7 @@ export async function verifyAndPlacePhonePeOrderAction(input) {
|
|
|
1462
1473
|
if (product)
|
|
1463
1474
|
productByIdPaid.set(pid, product);
|
|
1464
1475
|
});
|
|
1465
|
-
const productChecks =
|
|
1476
|
+
const productChecks = settleItems.map((item) => {
|
|
1466
1477
|
const [firstMember] = getCartItemMemberIds(item);
|
|
1467
1478
|
const product = productByIdPaid.get(firstMember) ?? null;
|
|
1468
1479
|
return { item, product };
|
|
@@ -1481,7 +1492,7 @@ export async function verifyAndPlacePhonePeOrderAction(input) {
|
|
|
1481
1492
|
runSyncPreflight(preflightPairs, "online");
|
|
1482
1493
|
// SB-UNI-O 2026-05-15 — Live-item jurisdiction guard.
|
|
1483
1494
|
if (!isDigitalCartPp && resolvedAddressPp) {
|
|
1484
|
-
assertLiveJurisdiction(
|
|
1495
|
+
assertLiveJurisdiction(settleItems, productByIdPaid, resolvedAddressPp.state);
|
|
1485
1496
|
}
|
|
1486
1497
|
// SB-UNI-5 — validate every required member product across the cart with
|
|
1487
1498
|
// cumulative decrement awareness (two bundles sharing a member must NOT
|
|
@@ -1491,7 +1502,7 @@ export async function verifyAndPlacePhonePeOrderAction(input) {
|
|
|
1491
1502
|
// ALREADY been captured for the full cart by the time this runs — so
|
|
1492
1503
|
// "skip_items" here means placing orders for the available items only and
|
|
1493
1504
|
// auto-refunding the dropped items' value (below), not skipping payment.
|
|
1494
|
-
const bucketedPaid = bucketCartItemsByStock(
|
|
1505
|
+
const bucketedPaid = bucketCartItemsByStock(settleItems, productByIdPaid, expansionPaid.decrements, (item) => productByIdPaid.get(getCartItemMemberIds(item)[0]) ?? null);
|
|
1495
1506
|
const unavailablePaid = bucketedPaid.unavailable;
|
|
1496
1507
|
if (unavailablePaid.length > 0) {
|
|
1497
1508
|
for (const u of unavailablePaid) {
|
|
@@ -73,6 +73,31 @@ export declare function laneCounts(items: readonly LaneAssignable[]): Record<Car
|
|
|
73
73
|
*/
|
|
74
74
|
export declare function activeLane(items: readonly LaneAssignable[]): CartLane | null;
|
|
75
75
|
export declare function isLaneCheckoutable(items: readonly LaneAssignable[], lane: CartLane): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* The items a checkout settles when the buyer has made no explicit selection.
|
|
78
|
+
*
|
|
79
|
+
* 🛑 This MUST be the active lane, never the whole cart. Both placement paths
|
|
80
|
+
* used to fall back to `cart.items`, and `assertCheckoutLane` then compared the
|
|
81
|
+
* whole cart against its own active lane — so every off-lane line was a
|
|
82
|
+
* violation and the throw was unconditional. The consequence was total: a cart
|
|
83
|
+
* holding more than ONE lane could not be checked out by any UI path, with
|
|
84
|
+
* `CHECKOUT_LANE_BLOCKED` and "Settle your 1 won auction first" returned for
|
|
85
|
+
* the very lane the buyer was already settling.
|
|
86
|
+
*
|
|
87
|
+
* Multi-lane carts are not an edge case — they are what the lane model
|
|
88
|
+
* produces. `auctionSettlement` pushes a won auction straight into the cart
|
|
89
|
+
* whatever is already sitting in it, and `assertCanAddNewItems` only blocks
|
|
90
|
+
* ADDING while a higher lane is pending; it never clears what was there first.
|
|
91
|
+
* So any buyer who had anything in their cart when they won an auction was
|
|
92
|
+
* locked out of checkout entirely. That is Root Cause #60's symptom returning
|
|
93
|
+
* by a different mechanism — the auction winner who cannot pay.
|
|
94
|
+
*
|
|
95
|
+
* Returning the active lane also makes the server agree with what the buyer is
|
|
96
|
+
* looking at: the cart page scopes its lines, its totals and its CTA to that
|
|
97
|
+
* same lane, and renders no selection control at all, so "no explicit
|
|
98
|
+
* selection" means "the lane on screen" and can mean nothing else.
|
|
99
|
+
*/
|
|
100
|
+
export declare function defaultCheckoutItems<T extends LaneAssignable>(items: readonly T[]): T[];
|
|
76
101
|
/**
|
|
77
102
|
* Why `lane` can't be checked out right now — null when it can.
|
|
78
103
|
* Phrased for the buyer, naming the lane that's blocking and what to do.
|
|
@@ -128,6 +128,34 @@ export function activeLane(items) {
|
|
|
128
128
|
export function isLaneCheckoutable(items, lane) {
|
|
129
129
|
return activeLane(items) === lane;
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* The items a checkout settles when the buyer has made no explicit selection.
|
|
133
|
+
*
|
|
134
|
+
* 🛑 This MUST be the active lane, never the whole cart. Both placement paths
|
|
135
|
+
* used to fall back to `cart.items`, and `assertCheckoutLane` then compared the
|
|
136
|
+
* whole cart against its own active lane — so every off-lane line was a
|
|
137
|
+
* violation and the throw was unconditional. The consequence was total: a cart
|
|
138
|
+
* holding more than ONE lane could not be checked out by any UI path, with
|
|
139
|
+
* `CHECKOUT_LANE_BLOCKED` and "Settle your 1 won auction first" returned for
|
|
140
|
+
* the very lane the buyer was already settling.
|
|
141
|
+
*
|
|
142
|
+
* Multi-lane carts are not an edge case — they are what the lane model
|
|
143
|
+
* produces. `auctionSettlement` pushes a won auction straight into the cart
|
|
144
|
+
* whatever is already sitting in it, and `assertCanAddNewItems` only blocks
|
|
145
|
+
* ADDING while a higher lane is pending; it never clears what was there first.
|
|
146
|
+
* So any buyer who had anything in their cart when they won an auction was
|
|
147
|
+
* locked out of checkout entirely. That is Root Cause #60's symptom returning
|
|
148
|
+
* by a different mechanism — the auction winner who cannot pay.
|
|
149
|
+
*
|
|
150
|
+
* Returning the active lane also makes the server agree with what the buyer is
|
|
151
|
+
* looking at: the cart page scopes its lines, its totals and its CTA to that
|
|
152
|
+
* same lane, and renders no selection control at all, so "no explicit
|
|
153
|
+
* selection" means "the lane on screen" and can mean nothing else.
|
|
154
|
+
*/
|
|
155
|
+
export function defaultCheckoutItems(items) {
|
|
156
|
+
const active = activeLane(items);
|
|
157
|
+
return active ? laneItems(items, active) : [...items];
|
|
158
|
+
}
|
|
131
159
|
/**
|
|
132
160
|
* Why `lane` can't be checked out right now — null when it can.
|
|
133
161
|
* Phrased for the buyer, naming the lane that's blocking and what to do.
|