@mohasinac/appkit 4.11.3 → 4.12.0

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.
Files changed (103) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +36 -3
  2. package/dist/_internal/server/features/checkout/actions.js +122 -44
  3. package/dist/_internal/server/features/orders/adapters.js +13 -0
  4. package/dist/_internal/server/features/products/list-public.d.ts +28 -0
  5. package/dist/_internal/server/features/products/list-public.js +92 -0
  6. package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
  7. package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
  8. package/dist/_internal/shared/checkout/lanes.js +28 -0
  9. package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
  10. package/dist/_internal/shared/features/checkout/config.js +8 -0
  11. package/dist/_internal/shared/fees/calculator.d.ts +34 -2
  12. package/dist/_internal/shared/fees/calculator.js +45 -1
  13. package/dist/client.d.ts +7 -1
  14. package/dist/client.js +4 -1
  15. package/dist/constants/api-endpoints.d.ts +6 -0
  16. package/dist/constants/api-endpoints.js +2 -0
  17. package/dist/features/admin/components/AdminAddressesView.js +18 -10
  18. package/dist/features/admin/components/AdminArtView.d.ts +9 -0
  19. package/dist/features/admin/components/AdminArtView.js +11 -35
  20. package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
  21. package/dist/features/admin/components/AdminClassifiedView.js +11 -35
  22. package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
  23. package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
  24. package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
  25. package/dist/features/admin/components/AdminLiveView.js +11 -41
  26. package/dist/features/admin/components/AdminOrderEditorView.d.ts +8 -1
  27. package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
  28. package/dist/features/admin/components/AdminOrdersView.js +14 -1
  29. package/dist/features/admin/components/AdminPaymentMethodsView.js +15 -10
  30. package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
  31. package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
  32. package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
  33. package/dist/features/admin/components/AdminStickersView.js +11 -35
  34. package/dist/features/admin/hooks/useAdminListing.js +7 -2
  35. package/dist/features/admin/schemas/firestore.d.ts +5 -0
  36. package/dist/features/admin/schemas/firestore.js +1 -0
  37. package/dist/features/auctions/actions/bid-actions.js +2 -2
  38. package/dist/features/cart/components/CartDrawer.d.ts +9 -1
  39. package/dist/features/cart/components/CartDrawer.js +4 -3
  40. package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
  41. package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
  42. package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
  43. package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
  44. package/dist/features/cart/components/index.d.ts +4 -0
  45. package/dist/features/cart/components/index.js +2 -0
  46. package/dist/features/cart/repository/cart.repository.d.ts +20 -2
  47. package/dist/features/cart/repository/cart.repository.js +49 -0
  48. package/dist/features/cart/schemas/firestore.d.ts +26 -0
  49. package/dist/features/layout/BottomActions.js +37 -4
  50. package/dist/features/layout/BottomActionsContext.d.ts +12 -0
  51. package/dist/features/layout/BottomActionsContext.js +5 -0
  52. package/dist/features/layout/hooks/useBottomActions.d.ts +6 -0
  53. package/dist/features/layout/hooks/useBottomActions.js +11 -1
  54. package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
  55. package/dist/features/orders/components/OrderAddonBadges.js +28 -0
  56. package/dist/features/orders/components/index.d.ts +1 -0
  57. package/dist/features/orders/components/index.js +1 -0
  58. package/dist/features/orders/repository/orders.repository.d.ts +32 -0
  59. package/dist/features/orders/repository/orders.repository.js +14 -0
  60. package/dist/features/orders/types/index.d.ts +22 -0
  61. package/dist/features/products/components/ArtStickersListView.js +5 -4
  62. package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
  63. package/dist/features/products/components/ProductsIndexListing.js +87 -11
  64. package/dist/features/products/components/ProductsIndexPageView.js +14 -2
  65. package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
  66. package/dist/features/products/config/listing-type-listing-config.js +74 -0
  67. package/dist/features/products/hooks/useProducts.js +14 -0
  68. package/dist/features/products/types/index.d.ts +15 -0
  69. package/dist/features/seller/components/SellerOrdersView.js +2 -1
  70. package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
  71. package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
  72. package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
  73. package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
  74. package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
  75. package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
  76. package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
  77. package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
  78. package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
  79. package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
  80. package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
  81. package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
  82. package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
  83. package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
  84. package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
  85. package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
  86. package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
  87. package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
  88. package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
  89. package/dist/features/stores/components/StoreProductsListing.js +3 -2
  90. package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
  91. package/dist/features/stores/components/StoreProductsPageView.js +16 -21
  92. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +602 -0
  93. package/dist/next/routing/route-map.d.ts +2 -0
  94. package/dist/next/routing/route-map.js +5 -0
  95. package/dist/seed/reviews-seed-data.js +52 -0
  96. package/dist/server-entry.d.ts +1 -0
  97. package/dist/server-entry.js +1 -0
  98. package/dist/styles.css +12 -1
  99. package/dist/tailwind-utilities.css +1 -1
  100. package/dist/ui/components/Checkbox.d.ts +10 -1
  101. package/dist/ui/components/Checkbox.js +14 -2
  102. package/dist/ui/components/Checkbox.style.css +11 -0
  103. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { type CartLane } from "../../../shared/checkout/lanes";
1
2
  import type { OutOfStockPolicy } from "../../../../features/orders/schemas/index";
2
3
  import { type CheckoutPaymentMethod } from "../../../shared/features/checkout/config";
3
4
  import { type CheckoutOrderResult } from "./data";
@@ -61,9 +62,29 @@ export interface CheckoutPricingPreviewInput {
61
62
  addressId?: string;
62
63
  paymentMethod: CheckoutPaymentMethod;
63
64
  excludedProductIds?: string[];
64
- whatsappNotifyAddon?: boolean;
65
- giftWrapAddon?: boolean;
66
- shipmentProtectionAddon?: boolean;
65
+ /**
66
+ * Which cart lane to price. Only one lane is payable in a single checkout, so
67
+ * a preview spanning lanes would be a number the buyer can never be charged.
68
+ *
69
+ * Passed in rather than derived from `activeLane` server-side because the cart
70
+ * page previews whichever tab the buyer is looking at — including a lane that
71
+ * is currently gated off.
72
+ */
73
+ lane?: CartLane;
74
+ }
75
+ /** One store's slice of the preview — what that seller's order will cost. */
76
+ export interface CheckoutPricingPreviewStore {
77
+ storeId: string;
78
+ storeName: string;
79
+ subtotal: number;
80
+ shippingFee: number;
81
+ codHandlingFee: number;
82
+ whatsappNotifyFee: number;
83
+ giftWrapFee: number;
84
+ shipmentProtectionFee: number;
85
+ gstAmount: number;
86
+ couponDiscount: number;
87
+ total: number;
67
88
  }
68
89
  export interface CheckoutPricingPreview {
69
90
  subtotal: number;
@@ -74,7 +95,19 @@ export interface CheckoutPricingPreview {
74
95
  shipmentProtectionFee: number;
75
96
  gstAmount: number;
76
97
  couponDiscount: number;
98
+ /** Buyer-facing platform commission, capped, charged once for this checkout. */
99
+ platformFee: number;
100
+ /** GST on the (capped) platform commission. */
101
+ gstOnFee: number;
77
102
  total: number;
103
+ /**
104
+ * Per-store breakdown, in group order. Every figure here was already being
105
+ * computed inside the per-group loop and then discarded into an aggregate —
106
+ * surfacing it is what lets each seller card show its own fees.
107
+ *
108
+ * A store with no selected items forms no group and therefore never appears.
109
+ */
110
+ stores: CheckoutPricingPreviewStore[];
78
111
  }
79
112
  /**
80
113
  * Read-only "what will I actually be charged" preview for the checkout
@@ -20,7 +20,7 @@ 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
22
  import { activeLane, laneOf } from "../../../shared/checkout/lanes";
23
- import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, } from "../../../shared/fees/calculator";
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";
26
26
  import { CART_COLLECTION } from "../../../../features/cart/schemas/index";
@@ -367,9 +367,12 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
367
367
  * so the caller can accumulate the checkout-wide `total`.
368
368
  */
369
369
  async function createOrderForGroup(group, orderType, ctx) {
370
- const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, whatsappNotifyAddon, giftWrapAddon, giftWrapMessage, shipmentProtectionAddon, } = ctx;
370
+ const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, storeAddons, platformFeeByStore, } = ctx;
371
371
  const firstItem = group[0].item;
372
372
  const firstProduct = group[0].product;
373
+ // Add-ons are this store's choice, not the cart's — see OrderGroupContext.
374
+ const { whatsappNotifyAddon = false, giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, } = storeAddons?.[firstItem.storeId] ?? {};
375
+ const { platformFee = 0, gstOnFee: platformFeeGst = 0 } = platformFeeByStore.get(firstItem.storeId) ?? {};
373
376
  const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
374
377
  // S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
375
378
  const orderItems = group.map(({ item, product }) => {
@@ -456,7 +459,10 @@ async function createOrderForGroup(group, orderType, ctx) {
456
459
  groupCouponCodes.add(discount.code);
457
460
  }
458
461
  }
459
- const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + (emiSchedule?.surchargeAmount ?? 0) + (gstBreakdown?.gstAmount ?? 0);
462
+ // The platform commission is now charged on EVERY payment method. It used to
463
+ // be added only on the Razorpay path, so COD / UPI-manual / cash / EMI buyers
464
+ // silently never paid it.
465
+ const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0) + (gstBreakdown?.gstAmount ?? 0);
460
466
  const imageUrls = [
461
467
  ...new Set(group
462
468
  .map(({ product }) => product.mainImage)
@@ -510,6 +516,9 @@ async function createOrderForGroup(group, orderType, ctx) {
510
516
  depositAmount: adminBypass ? undefined : depositAmount,
511
517
  codRemainingAmount: adminBypass ? undefined : codRemainingAmount,
512
518
  codHandlingFee: !adminBypass && codHandlingFee > 0 ? codHandlingFee : undefined,
519
+ // This group's pro-rata share of the checkout's single capped commission —
520
+ // the shares across a multi-store checkout sum to exactly what was charged.
521
+ platformFee: !adminBypass && platformFee > 0 ? platformFee : undefined,
513
522
  whatsappNotifyAddon: !adminBypass && whatsappNotifyFee > 0 ? true : undefined,
514
523
  whatsappNotifyFee: !adminBypass && whatsappNotifyFee > 0 ? whatsappNotifyFee : undefined,
515
524
  giftWrapAddon: !adminBypass && giftWrapFee > 0 ? true : undefined,
@@ -591,7 +600,7 @@ async function createOrderForGroup(group, orderType, ctx) {
591
600
  * auth + rate-limiting before calling this and supply the resolved user fields.
592
601
  */
593
602
  export async function createCheckoutOrderAction(input) {
594
- const { userId: uid, userName, userEmail, addressId, paymentMethod, emiTenureMonths, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, outOfStockPolicy = OutOfStockPolicyValues.SKIP_ITEMS, whatsappNotifyAddon = false, giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, } = input;
603
+ const { userId: uid, userName, userEmail, addressId, paymentMethod, emiTenureMonths, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, outOfStockPolicy = OutOfStockPolicyValues.SKIP_ITEMS, } = input;
595
604
  const siteSettings = await siteSettingsRepository.getSingleton();
596
605
  const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
597
606
  const emiSettings = siteSettings?.emi ?? CHECKOUT_DEFAULT_EMI_SETTINGS;
@@ -798,6 +807,12 @@ export async function createCheckoutOrderAction(input) {
798
807
  const emailsToSend = [];
799
808
  const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
800
809
  const couponUsageAccumulator = new Map();
810
+ // One commission on one transaction, capped, then apportioned across the
811
+ // orders this cart splits into — see allocateCheckoutFees.
812
+ const platformFeeByStore = allocateCheckoutFees(orderGroups.map(({ items: g }) => [
813
+ g[0].item.storeId,
814
+ g.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0),
815
+ ]), computeCheckoutFees(cartSubtotal, commissions));
801
816
  const groupCtx = {
802
817
  paymentMethod,
803
818
  emiTenureMonths,
@@ -821,10 +836,8 @@ export async function createCheckoutOrderAction(input) {
821
836
  buyerState: resolvedAddress?.state,
822
837
  gstSettings: siteSettings?.gst,
823
838
  siteContactUpiVpa: siteSettings?.contact?.upiVpa,
824
- whatsappNotifyAddon,
825
- giftWrapAddon,
826
- giftWrapMessage,
827
- shipmentProtectionAddon,
839
+ storeAddons: cart.storeAddons,
840
+ platformFeeByStore,
828
841
  };
829
842
  for (const { items: group, orderType } of orderGroups) {
830
843
  total += await createOrderForGroup(group, orderType, groupCtx);
@@ -850,7 +863,10 @@ const EMPTY_PRICING_PREVIEW = {
850
863
  shipmentProtectionFee: 0,
851
864
  gstAmount: 0,
852
865
  couponDiscount: 0,
866
+ platformFee: 0,
867
+ gstOnFee: 0,
853
868
  total: 0,
869
+ stores: [],
854
870
  };
855
871
  /**
856
872
  * Read-only "what will I actually be charged" preview for the checkout
@@ -862,13 +878,16 @@ const EMPTY_PRICING_PREVIEW = {
862
878
  * stock, creates an order, or writes coupon usage.
863
879
  */
864
880
  export async function previewCheckoutPricing(input) {
865
- const { userId: uid, addressId, paymentMethod, excludedProductIds = [], whatsappNotifyAddon = false, giftWrapAddon = false, shipmentProtectionAddon = false, } = input;
881
+ const { userId: uid, addressId, paymentMethod, excludedProductIds = [], lane, } = input;
866
882
  const siteSettings = await siteSettingsRepository.getSingleton();
867
883
  const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
868
884
  const cart = await unitOfWork.carts.getOrCreate(uid);
869
885
  const excludedSet = new Set(excludedProductIds);
870
886
  const selectedSet = cart.selectedItemIds?.length ? new Set(cart.selectedItemIds) : null;
871
- const previewLane = activeLane(cart.items ?? []);
887
+ // An explicit lane wins: the cart page prices whichever tab is open, which may
888
+ // not be the one that is currently payable. Falling back to activeLane keeps
889
+ // the checkout page's existing behaviour unchanged.
890
+ const previewLane = lane ?? activeLane(cart.items ?? []);
872
891
  const cartItems = (cart.items ?? []).filter((item) => !excludedSet.has(item.productId) &&
873
892
  (!selectedSet || selectedSet.has(item.itemId)) &&
874
893
  // A preview never throws — it just shows the lane the buyer can actually
@@ -894,45 +913,75 @@ export async function previewCheckoutPricing(input) {
894
913
  // Same re-validation the real order placement runs, so the preview never
895
914
  // shows a discount the order won't actually honour.
896
915
  const { coupons: appliedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], checks, uid);
897
- let shippingFee = 0;
898
- let codHandlingFee = 0;
899
- let whatsappNotifyFeeTotal = 0;
900
- let giftWrapFeeTotal = 0;
901
- let shipmentProtectionFeeTotal = 0;
902
- let gstAmount = 0;
903
- let couponDiscount = 0;
916
+ // Each group's figures are kept, not just summed away — the aggregates below
917
+ // are derived FROM this array, so a store card and the summary panel can never
918
+ // disagree about the same fee.
919
+ const stores = [];
904
920
  for (const { items: group } of orderGroups) {
905
921
  const firstItem = group[0].item;
906
922
  const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
907
923
  const { shippingFee: groupShippingFee, storeState } = await resolveShippingCost(firstItem.storeId);
908
- shippingFee += groupShippingFee;
924
+ // Add-ons are per store, read off the cart document — the same entry
925
+ // createOrderForGroup will read when the order is actually placed.
926
+ const addons = cart.storeAddons?.[firstItem.storeId] ?? {};
909
927
  // Mirrors createOrderForGroup exactly — the COD handling fee is only
910
928
  // ever charged for literal "cod" (pay the courier), never upi_manual or
911
929
  // cash (both pre-paid via UPI/bank transfer before dispatch).
912
- if (paymentMethod === "cod")
913
- codHandlingFee += computeCodHandlingFee(groupTotal, commissions);
914
- whatsappNotifyFeeTotal += computeWhatsAppNotifyFee(whatsappNotifyAddon, commissions);
915
- giftWrapFeeTotal += computeGiftWrapFee(giftWrapAddon, commissions);
916
- shipmentProtectionFeeTotal += computeShipmentProtectionFee(groupTotal, shipmentProtectionAddon, commissions);
930
+ const groupCodHandlingFee = paymentMethod === "cod" ? computeCodHandlingFee(groupTotal, commissions) : 0;
931
+ const groupWhatsappFee = computeWhatsAppNotifyFee(addons.whatsappNotifyAddon ?? false, commissions);
932
+ const groupGiftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissions);
933
+ const groupShipmentProtectionFee = computeShipmentProtectionFee(groupTotal, addons.shipmentProtectionAddon ?? false, commissions);
934
+ let groupGstAmount = 0;
917
935
  if (siteSettings?.gst?.enabled && resolvedAddress?.state) {
918
936
  const intraState = !!storeState && storeState === resolvedAddress.state;
919
937
  for (const { item, product } of group) {
920
938
  const lineTotal = unitPriceFor(item, product) * item.quantity;
921
939
  const rate = product?.gstRate ?? 0;
922
940
  if (rate > 0)
923
- gstAmount += calculateGst(rate, intraState, lineTotal).gstAmount;
941
+ groupGstAmount += calculateGst(rate, intraState, lineTotal).gstAmount;
924
942
  }
925
943
  }
926
944
  const groupLines = group.map(({ item, product }) => ({ itemId: item.itemId, lineTotal: unitPriceFor(item, product) * item.quantity }));
927
945
  const { couponDiscount: groupCouponDiscount } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
928
- couponDiscount += groupCouponDiscount;
946
+ stores.push({
947
+ storeId: firstItem.storeId,
948
+ storeName: firstItem.storeName || firstItem.storeId,
949
+ subtotal: groupTotal,
950
+ shippingFee: groupShippingFee,
951
+ codHandlingFee: groupCodHandlingFee,
952
+ whatsappNotifyFee: groupWhatsappFee,
953
+ giftWrapFee: groupGiftWrapFee,
954
+ shipmentProtectionFee: groupShipmentProtectionFee,
955
+ gstAmount: groupGstAmount,
956
+ couponDiscount: groupCouponDiscount,
957
+ total: Math.max(0, groupTotal - groupCouponDiscount) +
958
+ groupShippingFee +
959
+ groupCodHandlingFee +
960
+ groupWhatsappFee +
961
+ groupGiftWrapFee +
962
+ groupShipmentProtectionFee +
963
+ groupGstAmount,
964
+ });
929
965
  }
966
+ const sum = (pick) => stores.reduce((acc, s) => acc + pick(s), 0);
967
+ const shippingFee = sum((s) => s.shippingFee);
968
+ const codHandlingFee = sum((s) => s.codHandlingFee);
969
+ const whatsappNotifyFeeTotal = sum((s) => s.whatsappNotifyFee);
970
+ const giftWrapFeeTotal = sum((s) => s.giftWrapFee);
971
+ const shipmentProtectionFeeTotal = sum((s) => s.shipmentProtectionFee);
972
+ const gstAmount = sum((s) => s.gstAmount);
973
+ const couponDiscount = sum((s) => s.couponDiscount);
974
+ // One capped commission for the whole checkout, on every payment method —
975
+ // deliberately not per store, so it is not added to the per-store rows above.
976
+ const { platformFee, gstOnFee } = computeCheckoutFees(cartSubtotal, commissions);
930
977
  const total = Math.max(0, cartSubtotal - couponDiscount) +
931
978
  shippingFee +
932
979
  codHandlingFee +
933
980
  whatsappNotifyFeeTotal +
934
981
  giftWrapFeeTotal +
935
982
  shipmentProtectionFeeTotal +
983
+ platformFee +
984
+ gstOnFee +
936
985
  gstAmount;
937
986
  return {
938
987
  subtotal: cartSubtotal,
@@ -943,7 +992,10 @@ export async function previewCheckoutPricing(input) {
943
992
  shipmentProtectionFee: shipmentProtectionFeeTotal,
944
993
  gstAmount,
945
994
  couponDiscount,
995
+ platformFee,
996
+ gstOnFee,
946
997
  total,
998
+ stores,
947
999
  };
948
1000
  }
949
1001
  /**
@@ -1064,7 +1116,7 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
1064
1116
  * from the inline version this replaced.
1065
1117
  */
1066
1118
  async function createRazorpayGroupOrder(group, orderType, ctx) {
1067
- const { appliedCoupons, cartSubtotal, platformFeePercent, gstPercent, whatsappNotifyFee, giftWrapFee, giftWrapMessage, shipmentProtectionAddon, siteSettings, uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, couponUsageAccumulator, } = ctx;
1119
+ const { appliedCoupons, cartSubtotal, storeAddons, platformFeeByStore, siteSettings, uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, couponUsageAccumulator, } = ctx;
1068
1120
  const firstItem = group[0].item;
1069
1121
  const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
1070
1122
  // Reuses the same store/seller lookup as the COD/UPI path above instead
@@ -1083,11 +1135,21 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
1083
1135
  groupCouponCodes.add(discount.code);
1084
1136
  }
1085
1137
  }
1086
- const rawPlatformFee = roundRupees(groupTotal * (platformFeePercent / 100));
1087
- const gstOnFee = roundRupees(rawPlatformFee * (gstPercent / 100));
1138
+ // Was a per-group, uncapped `groupTotal × platformFeePercent` computed inline
1139
+ // here the only path that charged the commission at all, and it charged a
1140
+ // different (larger, multiplied-per-store) number than the cap now allows.
1141
+ // Both the amount and its allocation are now decided once for the checkout.
1142
+ const { platformFee: rawPlatformFee = 0, gstOnFee = 0 } = platformFeeByStore.get(firstItem.storeId) ?? {};
1088
1143
  const platformFee = rawPlatformFee + gstOnFee;
1089
- const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, shipmentProtectionAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
1090
- const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee;
1144
+ // Add-ons are this store's choice, read off the cart doc — previously one
1145
+ // cart-wide fee was passed in and billed against every group.
1146
+ const commissionRates = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
1147
+ const addons = storeAddons?.[firstItem.storeId] ?? {};
1148
+ const whatsappNotifyFee = computeWhatsAppNotifyFee(addons.whatsappNotifyAddon ?? false, commissionRates);
1149
+ const giftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates);
1150
+ const giftWrapMessage = addons.giftWrapMessage;
1151
+ const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, addons.shipmentProtectionAddon ?? false, commissionRates);
1152
+ const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + platformFee;
1091
1153
  // P-8 GST — deliberately NOT wired into this Razorpay-verify path. The
1092
1154
  // amount-mismatch check above (expectedPaymentAmountRs) compares against
1093
1155
  // what the buyer already paid via the Razorpay order created earlier in
@@ -1232,12 +1294,11 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
1232
1294
  return orderTotal;
1233
1295
  }
1234
1296
  export async function verifyAndPlaceRazorpayOrderAction(input) {
1235
- const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, outOfStockPolicy = OutOfStockPolicyValues.CANCEL_ORDER, whatsappNotifyAddon = false, giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, } = input;
1297
+ const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, outOfStockPolicy = OutOfStockPolicyValues.CANCEL_ORDER, } = input;
1298
+ // Add-on fees are no longer resolved here. They are per store, and this
1299
+ // function has no single store — each group reads its own selection off
1300
+ // `cart.storeAddons` inside createRazorpayGroupOrder.
1236
1301
  const siteSettings = await siteSettingsRepository.getSingleton();
1237
- const platformFeePercent = siteSettings?.commissions?.platformFeePercent ?? 5;
1238
- const gstPercent = siteSettings?.commissions?.gstPercent ?? 18;
1239
- const whatsappNotifyFee = computeWhatsAppNotifyFee(whatsappNotifyAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
1240
- const giftWrapFee = computeGiftWrapFee(giftWrapAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
1241
1302
  const isValid = await verifyPaymentSignatureWithKeys({
1242
1303
  razorpay_order_id,
1243
1304
  razorpay_payment_id,
@@ -1360,8 +1421,12 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
1360
1421
  const unit = isBundle ? item.price : product.price;
1361
1422
  return sum + unit * item.quantity;
1362
1423
  }, 0);
1363
- const expectedPlatformFee = roundRupees(cartSubtotalRs * (platformFeePercent / 100));
1364
- const expectedGstOnFee = roundRupees(expectedPlatformFee * (gstPercent / 100));
1424
+ // Must use the SAME helper /api/payment/create-order used to compute what
1425
+ // it charged. This block used to re-derive the fee inline from the raw
1426
+ // percentages, so the moment the cap landed it would have expected more
1427
+ // than was actually collected and rejected every legitimate payment.
1428
+ const commissionRates = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
1429
+ const { platformFee: expectedPlatformFee, gstOnFee: expectedGstOnFee } = computeCheckoutFees(cartSubtotalRs, commissionRates);
1365
1430
  // Same per-seller-group shipping sum /api/payment/create-order charged
1366
1431
  // upfront (and the same resolveShippingCost createRazorpayGroupOrder
1367
1432
  // below will use to build each order's recorded totalPrice) — keeps this
@@ -1369,7 +1434,19 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
1369
1434
  const expectedShippingGroups = splitCartIntoOrderGroups(bucketedPaid.available);
1370
1435
  const expectedShippingFees = await Promise.all(expectedShippingGroups.map((g) => resolveShippingCost(g.items[0].item.storeId)));
1371
1436
  const expectedShippingFee = expectedShippingFees.reduce((sum, r) => sum + r.shippingFee, 0);
1372
- const expectedPaymentAmountRs = cartSubtotalRs + expectedPlatformFee + expectedGstOnFee + expectedShippingFee;
1437
+ // Add-ons are per store, so the expected total sums each group's own
1438
+ // selection — a single cart-wide figure would over- or under-count as soon
1439
+ // as the buyer picked add-ons for only some sellers.
1440
+ const expectedAddonFees = expectedShippingGroups.reduce((sum, g) => {
1441
+ const storeId = g.items[0].item.storeId;
1442
+ const addons = cart.storeAddons?.[storeId] ?? {};
1443
+ const groupSubtotal = g.items.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0);
1444
+ return (sum +
1445
+ computeWhatsAppNotifyFee(addons.whatsappNotifyAddon ?? false, commissionRates) +
1446
+ computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates) +
1447
+ computeShipmentProtectionFee(groupSubtotal, addons.shipmentProtectionAddon ?? false, commissionRates));
1448
+ }, 0);
1449
+ const expectedPaymentAmountRs = cartSubtotalRs + expectedPlatformFee + expectedGstOnFee + expectedAddonFees + expectedShippingFee;
1373
1450
  const rzpOrderRecord = await fetchRazorpayOrder(razorpay_order_id);
1374
1451
  const paidAmountRs = paiseToRupees(rzpOrderRecord.amount);
1375
1452
  if (paidAmountRs < expectedPaymentAmountRs - 1) {
@@ -1397,16 +1474,17 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
1397
1474
  const cartSubtotal = orderGroups.reduce((s, { items: g }) => s +
1398
1475
  g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
1399
1476
  const couponUsageAccumulator = new Map();
1477
+ // Same single capped commission + pro-rata split as the COD/UPI path.
1478
+ const platformFeeByStore = allocateCheckoutFees(orderGroups.map(({ items: g }) => [
1479
+ g[0].item.storeId,
1480
+ g.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0),
1481
+ ]), computeCheckoutFees(cartSubtotal, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS));
1400
1482
  for (const { items: group, orderType } of orderGroups) {
1401
1483
  total += await createRazorpayGroupOrder(group, orderType, {
1402
1484
  appliedCoupons,
1403
1485
  cartSubtotal,
1404
- platformFeePercent,
1405
- gstPercent,
1406
- whatsappNotifyFee,
1407
- giftWrapFee,
1408
- giftWrapMessage,
1409
- shipmentProtectionAddon,
1486
+ storeAddons: cart.storeAddons,
1487
+ platformFeeByStore,
1410
1488
  siteSettings,
1411
1489
  uid,
1412
1490
  userName,
@@ -62,7 +62,20 @@ export function orderDocumentToOrder(doc) {
62
62
  total: doc.totalPrice,
63
63
  currency: doc.currency,
64
64
  couponCode: doc.couponCode,
65
+ couponDiscount: doc.couponDiscount,
65
66
  appliedDiscounts: doc.appliedDiscounts,
67
+ // Add-ons are operational, not decorative: the status-change notifier reads
68
+ // whatsappNotifyAddon, and the packer needs giftWrapMessage in hand. An
69
+ // adapter that drops them makes every downstream surface render its
70
+ // "not applicable" fallback instead (Root Cause #57).
71
+ whatsappNotifyAddon: doc.whatsappNotifyAddon,
72
+ whatsappNotifyFee: doc.whatsappNotifyFee,
73
+ giftWrapAddon: doc.giftWrapAddon,
74
+ giftWrapFee: doc.giftWrapFee,
75
+ giftWrapMessage: doc.giftWrapMessage,
76
+ shipmentProtectionAddon: doc.shipmentProtectionAddon,
77
+ shipmentProtectionFee: doc.shipmentProtectionFee,
78
+ platformFee: doc.platformFee,
66
79
  trackingNumber: doc.trackingNumber,
67
80
  shippingCarrier: doc.shippingCarrier,
68
81
  trackingUrl: doc.trackingUrl,
@@ -49,6 +49,16 @@ export interface PublicProductListInput {
49
49
  freeShipping?: boolean;
50
50
  isPartOfBundle?: boolean;
51
51
  features?: readonly string[];
52
+ /** Pipe-joined multi-select over the `tags` array field. */
53
+ tags?: readonly string[];
54
+ /** Pipe-joined multi-select over `sublistingCategoryId`. */
55
+ sublistingCategoryIds?: readonly string[];
56
+ /**
57
+ * Per-listing-type facets (classified meetup city, digital-code delivery
58
+ * method, live-item species, …), keyed by TABLE_KEY. Resolved to their
59
+ * nested Firestore paths via `TYPE_FACET_FIELD`.
60
+ */
61
+ typeFacets?: Record<string, string>;
52
62
  preOrderProductionStatus?: string;
53
63
  prizeRevealStatus?: string;
54
64
  /** Hide sold-out rows. Applied in memory — never pushed into Firestore. */
@@ -127,4 +137,22 @@ export declare function parsePublicProductParams(params: URLSearchParams | Recor
127
137
  hideEndedByDefault?: boolean;
128
138
  }): PublicProductListInput;
129
139
  export declare function listPublicProducts(input: PublicProductListInput, opts?: PublicProductListOptions): Promise<PublicProductListResult | null>;
140
+ /**
141
+ * One store tab's worth of listings.
142
+ *
143
+ * Every `Store*PageView` used to hand-roll this: `productRepository.list()`
144
+ * with an inline `sieveAnd(...)` and a `.catch(() => null)`. That is the exact
145
+ * shape that made `/art` render empty — an unsafe inequality (or any missing
146
+ * index) becomes FAILED_PRECONDITION, the catch turns it into `null`, and the
147
+ * page shows "no results" with nothing logged. Eight views carried it, and
148
+ * `audit-listing-filter-parity` had none of them registered.
149
+ *
150
+ * Routing them through `listPublicProducts` also gives every store tab
151
+ * something it never had: the URL's own sort/filter/page params reach the SSR
152
+ * fetch, so the first paint matches what the toolbar says is selected.
153
+ */
154
+ export declare function listStoreProducts(storeId: string, listingTypes: readonly string[], searchParams?: URLSearchParams | Record<string, string | string[] | undefined>, defaults?: {
155
+ pageSize?: number;
156
+ sorts?: string;
157
+ }): Promise<PublicProductListResult | null>;
130
158
  export {};
@@ -80,6 +80,40 @@ function num(raw) {
80
80
  const n = Number(raw);
81
81
  return Number.isFinite(n) ? n : undefined;
82
82
  }
83
+ /**
84
+ * Per-type facet URL key → the Firestore field path it filters.
85
+ *
86
+ * Every target is already allowlisted in the products repository's
87
+ * SIEVE_FIELDS — the nested `classified.*` / `digitalCode.*` / `liveItem.*`
88
+ * entries have been there since those types shipped. What was missing was any
89
+ * code path that actually emitted a clause for them: the store listings
90
+ * declared these keys in their FILTER_KEYS (so they counted toward the
91
+ * filter badge) and rendered the controls, but never put them on the wire.
92
+ */
93
+ const TYPE_FACET_FIELD = {
94
+ [TABLE_KEYS.CITY]: "classified.meetupArea.city",
95
+ [TABLE_KEYS.NEGOTIABLE]: "classified.negotiable",
96
+ [TABLE_KEYS.ACCEPTS_SHIPPING]: "classified.acceptsShipping",
97
+ [TABLE_KEYS.DELIVERY_METHOD]: "digitalCode.codeDeliveryMethod",
98
+ [TABLE_KEYS.SPECIES]: "liveItem.species",
99
+ [TABLE_KEYS.LIVE_SEX]: "liveItem.sex",
100
+ [TABLE_KEYS.JURISDICTION]: "liveItem.jurisdictionAllowed",
101
+ };
102
+ /** Read every known per-type facet present in the params. */
103
+ function readTypeFacets(get) {
104
+ const out = {};
105
+ for (const key of Object.keys(TYPE_FACET_FIELD)) {
106
+ const value = get(key);
107
+ if (value)
108
+ out[key] = value;
109
+ }
110
+ return Object.keys(out).length > 0 ? out : undefined;
111
+ }
112
+ /** Split a pipe-joined multi-select value; `undefined` when empty. */
113
+ function pipeList(raw) {
114
+ const parts = raw.split("|").map((s) => s.trim()).filter(Boolean);
115
+ return parts.length > 0 ? parts : undefined;
116
+ }
83
117
  /**
84
118
  * Read the shared public-listing query params. SSR views hand in Next's
85
119
  * `searchParams` record; the API route hands in `url.searchParams`. Both produce
@@ -118,6 +152,12 @@ export function parsePublicProductParams(params, defaults) {
118
152
  freeShipping: get(TABLE_KEYS.FREE_SHIPPING) === "true" || undefined,
119
153
  isPartOfBundle: get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true" || undefined,
120
154
  features: featuresRaw ? featuresRaw.split("|").filter(Boolean) : undefined,
155
+ // Tags and Sublisting Type were rendered by <ProductFilters> for all three
156
+ // variants and counted toward the active-filter badge, but were never
157
+ // parsed here — so ticking one inflated the badge and changed nothing.
158
+ tags: pipeList(get(TABLE_KEYS.TAGS)),
159
+ sublistingCategoryIds: pipeList(get(TABLE_KEYS.SUBLISTING_CATEGORY)),
160
+ typeFacets: readTypeFacets(get),
121
161
  preOrderProductionStatus: get(TABLE_KEYS.PREORDER_STATUS) || get("preOrderStatus") || undefined,
122
162
  prizeRevealStatus: get(TABLE_KEYS.PRIZE_REVEAL_STATUS) || undefined,
123
163
  // Three spellings of one intent — /products and /art call it "Show sold",
@@ -211,6 +251,31 @@ function buildFirestoreSafeFilters(input) {
211
251
  if (input.features?.length === 1) {
212
252
  parts.push(sieveFilter(PRODUCT_FIELDS.FEATURES, SIEVE_OP.CONTAINS, input.features[0]));
213
253
  }
254
+ // `tags` is an array field, so the same one-value limit applies.
255
+ if (input.tags?.length === 1) {
256
+ parts.push(sieveFilter(PRODUCT_FIELDS.TAGS, SIEVE_OP.CONTAINS, input.tags[0]));
257
+ }
258
+ // A scalar field, so several values ARE expressible as an OR-group (which
259
+ // the Firebase adapter upgrades to `in`).
260
+ if (input.sublistingCategoryIds?.length) {
261
+ parts.push(sieveFilter(PRODUCT_FIELDS.SUBLISTING_CATEGORY_ID, SIEVE_OP.EQ, input.sublistingCategoryIds.join("|")));
262
+ }
263
+ // Per-type facets. Unknown keys can't reach here — `readTypeFacets` only
264
+ // reads the ones in TYPE_FACET_FIELD, so an arbitrary query param can never
265
+ // be turned into a Firestore field path.
266
+ for (const [key, raw] of Object.entries(input.typeFacets ?? {})) {
267
+ const field = TYPE_FACET_FIELD[key];
268
+ if (!field || !raw)
269
+ continue;
270
+ // `jurisdictionAllowed` is an array field — array-contains, not equality.
271
+ const op = field.endsWith("jurisdictionAllowed") ? SIEVE_OP.CONTAINS : SIEVE_OP.EQ;
272
+ const values = raw.split("|").filter(Boolean);
273
+ if (values.length === 0)
274
+ continue;
275
+ parts.push(op === SIEVE_OP.CONTAINS
276
+ ? sieveFilter(field, op, values[0])
277
+ : sieveFilter(field, op, values.join("|")));
278
+ }
214
279
  if (input.rawFilters)
215
280
  parts.push(input.rawFilters);
216
281
  return sieveAnd(...parts.filter(Boolean));
@@ -353,6 +418,33 @@ export async function listPublicProducts(input, opts) {
353
418
  sorts,
354
419
  };
355
420
  }
421
+ /**
422
+ * One store tab's worth of listings.
423
+ *
424
+ * Every `Store*PageView` used to hand-roll this: `productRepository.list()`
425
+ * with an inline `sieveAnd(...)` and a `.catch(() => null)`. That is the exact
426
+ * shape that made `/art` render empty — an unsafe inequality (or any missing
427
+ * index) becomes FAILED_PRECONDITION, the catch turns it into `null`, and the
428
+ * page shows "no results" with nothing logged. Eight views carried it, and
429
+ * `audit-listing-filter-parity` had none of them registered.
430
+ *
431
+ * Routing them through `listPublicProducts` also gives every store tab
432
+ * something it never had: the URL's own sort/filter/page params reach the SSR
433
+ * fetch, so the first paint matches what the toolbar says is selected.
434
+ */
435
+ export async function listStoreProducts(storeId, listingTypes, searchParams = {}, defaults) {
436
+ return listPublicProducts({
437
+ ...parsePublicProductParams(searchParams, {
438
+ listingTypes,
439
+ pageSize: defaults?.pageSize ?? DEFAULT_PAGE_SIZE,
440
+ sorts: defaults?.sorts,
441
+ ...defaultTogglesForListingTypes(listingTypes),
442
+ }),
443
+ // The route owns the store identity — a `?storeId=` in the URL must not be
444
+ // able to make one store's tab render another store's inventory.
445
+ storeId,
446
+ });
447
+ }
356
448
  function sortRows(rows, field, desc) {
357
449
  return [...rows].sort((a, b) => {
358
450
  const av = a[field];
@@ -2,12 +2,15 @@ import { bidRepository, cartRepository, productRepository, storeRepository, } fr
2
2
  import { sendNotification } from "../../../../features/admin/actions/notification-actions";
3
3
  import { AUCTION_MESSAGES } from "../handlers/messages";
4
4
  import { ROUTES } from "../../../../next/routing/route-map";
5
- import { CART_LANE } from "../../../shared/checkout/lanes";
5
+ import { CART_LANE, LANE_CHECKOUT_WINDOW_MS } from "../../../shared/checkout/lanes";
6
6
  /**
7
7
  * How long the winner has to pay before the locked cart line lapses. Mirrors
8
8
  * the accepted-offer window so both locked lanes behave the same.
9
9
  */
10
- const AUCTION_CHECKOUT_WINDOW_MS = 48 * 60 * 60 * 1000;
10
+ // Was a local `48 * 60 * 60 * 1000` here AND an identical one in bid-actions.ts
11
+ // (the Buy-Now path), with nothing linking them — changing one would have given
12
+ // the same kind of win two different deadlines depending on how it was won.
13
+ const AUCTION_CHECKOUT_WINDOW_MS = LANE_CHECKOUT_WINDOW_MS.auction;
11
14
  /** Deep link the winner straight into the auction checkout lane. */
12
15
  const AUCTION_CHECKOUT_URL = `${String(ROUTES.USER.CHECKOUT)}?lane=${CART_LANE.AUCTION}`;
13
16
  async function settleAuction(ctx, product) {
@@ -11,6 +11,26 @@ export type CartLane = (typeof CART_LANE)[keyof typeof CART_LANE];
11
11
  */
12
12
  export declare const CART_LANE_PRIORITY: readonly CartLane[];
13
13
  export declare const CART_LANE_LABELS: Record<CartLane, string>;
14
+ /**
15
+ * How long a locked line survives before it lapses, per lane.
16
+ *
17
+ * Lives here, with the lane model, because the deadline is a property OF the
18
+ * lane — and because `AUCTION_CHECKOUT_WINDOW_MS` was previously declared twice
19
+ * (`auctionSettlement.ts` and `bid-actions.ts`, i.e. the settlement path and the
20
+ * Buy-Now path) with no link between them: changing one would silently give the
21
+ * same kind of win two different deadlines depending on how it was won.
22
+ *
23
+ * The two windows are equal today but mean different things, so they stay
24
+ * separate names rather than one shared constant:
25
+ * - **auction** — payment is MANDATORY. You bid, you won, you owe it. Past the
26
+ * deadline the bid is forfeited and the buyer is warned that repeated
27
+ * non-payment restricts their account.
28
+ * - **offer** — payment is OPTIONAL. Declining to buy at your own offer price
29
+ * is the buyer's right; the offer simply lapses with no penalty.
30
+ */
31
+ export declare const LANE_CHECKOUT_WINDOW_MS: Record<"auction" | "offer", number>;
32
+ /** 2 days, in hours — for buyer-facing copy ("pay within 48 hours"). */
33
+ export declare const LANE_CHECKOUT_WINDOW_HOURS: Record<"auction" | "offer", number>;
14
34
  /** The two lanes whose price is fixed outside the listing. */
15
35
  export declare function isLockedLane(lane: CartLane): boolean;
16
36
  /** Minimal shape this module needs — works on cart documents and client rows alike. */
@@ -44,6 +44,34 @@ export const CART_LANE_LABELS = {
44
44
  offer: "Accepted offers",
45
45
  standard: "Cart",
46
46
  };
47
+ /**
48
+ * How long a locked line survives before it lapses, per lane.
49
+ *
50
+ * Lives here, with the lane model, because the deadline is a property OF the
51
+ * lane — and because `AUCTION_CHECKOUT_WINDOW_MS` was previously declared twice
52
+ * (`auctionSettlement.ts` and `bid-actions.ts`, i.e. the settlement path and the
53
+ * Buy-Now path) with no link between them: changing one would silently give the
54
+ * same kind of win two different deadlines depending on how it was won.
55
+ *
56
+ * The two windows are equal today but mean different things, so they stay
57
+ * separate names rather than one shared constant:
58
+ * - **auction** — payment is MANDATORY. You bid, you won, you owe it. Past the
59
+ * deadline the bid is forfeited and the buyer is warned that repeated
60
+ * non-payment restricts their account.
61
+ * - **offer** — payment is OPTIONAL. Declining to buy at your own offer price
62
+ * is the buyer's right; the offer simply lapses with no penalty.
63
+ */
64
+ export const LANE_CHECKOUT_WINDOW_MS = {
65
+ /** 2 days. Mandatory — enforced by the expiry sweep, which forfeits the bid. */
66
+ auction: 48 * 60 * 60 * 1000,
67
+ /** 2 days. Optional — lapses quietly, no penalty. */
68
+ offer: 48 * 60 * 60 * 1000,
69
+ };
70
+ /** 2 days, in hours — for buyer-facing copy ("pay within 48 hours"). */
71
+ export const LANE_CHECKOUT_WINDOW_HOURS = {
72
+ auction: LANE_CHECKOUT_WINDOW_MS.auction / (60 * 60 * 1000),
73
+ offer: LANE_CHECKOUT_WINDOW_MS.offer / (60 * 60 * 1000),
74
+ };
47
75
  /** The two lanes whose price is fixed outside the listing. */
48
76
  export function isLockedLane(lane) {
49
77
  return lane === CART_LANE.AUCTION || lane === CART_LANE.OFFER;