@mohasinac/appkit 4.42.13 โ†’ 4.42.15

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
- const cartItems = cart.items.filter((item) => !excludedSet.has(item.productId) &&
659
- (!selectedSet || selectedSet.has(item.itemId)));
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
- // The PhonePe path settles the whole cart (no selectedItemIds), so the
1430
- // "selected" set is the cart itself.
1431
- assertCheckoutLane(cart.items, cart.items);
1432
- await assertLockedLinesStillValid(cart.items, uid);
1433
- const isDigitalCartPp = cartIsDigitalOnly(cart.items);
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(cart.items);
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 = cart.items.map((item) => {
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(cart.items, productByIdPaid, resolvedAddressPp.state);
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(cart.items, productByIdPaid, expansionPaid.decrements, (item) => productByIdPaid.get(getCartItemMemberIds(item)[0]) ?? null);
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.
@@ -69,9 +69,17 @@ export function AdminStoresView({ children, ...props }) {
69
69
  mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
70
70
  id: toStringValue(item.id, `store-${index}`),
71
71
  primary: toStringValue(item.storeName, "Unnamed store"),
72
+ // Verified / Featured are rendered here because an admin's first
73
+ // question about a store is whether it carries either, and the row
74
+ // is where that gets scanned. The API has always returned both
75
+ // (route.ts serialises isVerified/isFeatured) and the editor already
76
+ // seeds from them โ€” only the list never showed them, so the flags
77
+ // were invisible until you opened each store one at a time.
72
78
  secondary: [
73
79
  toStringValue(item.storeSlug, "No slug"),
74
80
  toStringValue(item.ownerId, "No owner"),
81
+ ...(item.isVerified ? ["Verified"] : []),
82
+ ...(item.isFeatured ? ["Featured"] : []),
75
83
  ].join(" ยท "),
76
84
  status: toStringValue(item.status, "Pending"),
77
85
  updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.42.13",
3
+ "version": "4.42.15",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"