@mohasinac/appkit 4.11.3 → 4.12.1

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 (160) hide show
  1. package/dist/_internal/server/features/auctions/service.d.ts +7 -1
  2. package/dist/_internal/server/features/auctions/service.js +13 -5
  3. package/dist/_internal/server/features/checkout/actions.d.ts +36 -18
  4. package/dist/_internal/server/features/checkout/actions.js +127 -45
  5. package/dist/_internal/server/features/orders/actions.js +5 -3
  6. package/dist/_internal/server/features/orders/adapters.js +13 -0
  7. package/dist/_internal/server/features/products/list-public.d.ts +28 -0
  8. package/dist/_internal/server/features/products/list-public.js +92 -0
  9. package/dist/_internal/server/functions/firestore.d.ts +1 -2
  10. package/dist/_internal/server/functions/firestore.js +1 -9
  11. package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
  12. package/dist/_internal/server/jobs/core/index.d.ts +0 -1
  13. package/dist/_internal/server/jobs/core/index.js +0 -1
  14. package/dist/_internal/server/jobs/core/offerExpiry.js +33 -1
  15. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -2
  16. package/dist/_internal/server/jobs/handlers/index.js +1 -2
  17. package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
  18. package/dist/_internal/shared/checkout/lanes.js +28 -0
  19. package/dist/_internal/shared/features/auctions/config.d.ts +34 -0
  20. package/dist/_internal/shared/features/auctions/config.js +47 -3
  21. package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
  22. package/dist/_internal/shared/features/checkout/config.js +8 -0
  23. package/dist/_internal/shared/fees/calculator.d.ts +34 -2
  24. package/dist/_internal/shared/fees/calculator.js +45 -1
  25. package/dist/_internal/shared/listing-types/auction/config.js +1 -1
  26. package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
  27. package/dist/_internal/shared/listing-types/live/config.js +1 -1
  28. package/dist/_internal/shared/listing-types/pre-order/config.js +1 -1
  29. package/dist/client.d.ts +7 -1
  30. package/dist/client.js +4 -1
  31. package/dist/constants/api-endpoints.d.ts +6 -0
  32. package/dist/constants/api-endpoints.js +2 -0
  33. package/dist/features/account/components/NotificationBell.js +1 -1
  34. package/dist/features/admin/components/AdminAddressesView.js +18 -10
  35. package/dist/features/admin/components/AdminArtView.d.ts +9 -0
  36. package/dist/features/admin/components/AdminArtView.js +11 -35
  37. package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
  38. package/dist/features/admin/components/AdminClassifiedView.js +11 -35
  39. package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
  40. package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
  41. package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
  42. package/dist/features/admin/components/AdminLiveView.js +11 -41
  43. package/dist/features/admin/components/AdminNotificationsView.js +4 -1
  44. package/dist/features/admin/components/AdminOrderEditorView.d.ts +14 -1
  45. package/dist/features/admin/components/AdminOrderEditorView.js +18 -5
  46. package/dist/features/admin/components/AdminOrdersView.js +15 -1
  47. package/dist/features/admin/components/AdminPaymentMethodsView.js +15 -10
  48. package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
  49. package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
  50. package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
  51. package/dist/features/admin/components/AdminStickersView.js +11 -35
  52. package/dist/features/admin/hooks/useAdminListing.js +7 -2
  53. package/dist/features/admin/schemas/firestore.d.ts +5 -0
  54. package/dist/features/admin/schemas/firestore.js +1 -0
  55. package/dist/features/auctions/actions/bid-actions.js +73 -11
  56. package/dist/features/auctions/components/AuctionDetailPageView.js +8 -2
  57. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
  58. package/dist/features/auctions/components/PlaceBidFormClient.js +89 -14
  59. package/dist/features/auctions/schemas/bid-input.d.ts +19 -6
  60. package/dist/features/auctions/schemas/bid-input.js +19 -7
  61. package/dist/features/auth/repository/user.repository.js +6 -0
  62. package/dist/features/cart/components/CartDrawer.d.ts +9 -1
  63. package/dist/features/cart/components/CartDrawer.js +4 -3
  64. package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
  65. package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
  66. package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
  67. package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
  68. package/dist/features/cart/components/index.d.ts +4 -0
  69. package/dist/features/cart/components/index.js +2 -0
  70. package/dist/features/cart/repository/cart.repository.d.ts +20 -2
  71. package/dist/features/cart/repository/cart.repository.js +49 -0
  72. package/dist/features/cart/schemas/firestore.d.ts +26 -0
  73. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  74. package/dist/features/filters/FilterFacetSection.js +1 -1
  75. package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
  76. package/dist/features/layout/BackToTop.js +1 -1
  77. package/dist/features/layout/BottomActions.js +38 -5
  78. package/dist/features/layout/BottomActionsContext.d.ts +12 -0
  79. package/dist/features/layout/BottomActionsContext.js +5 -0
  80. package/dist/features/layout/FooterLayout.js +2 -2
  81. package/dist/features/layout/TitleBarLayout.js +1 -1
  82. package/dist/features/layout/hooks/useBottomActions.d.ts +6 -0
  83. package/dist/features/layout/hooks/useBottomActions.js +11 -1
  84. package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
  85. package/dist/features/orders/components/OrderAddonBadges.js +28 -0
  86. package/dist/features/orders/components/index.d.ts +1 -0
  87. package/dist/features/orders/components/index.js +1 -0
  88. package/dist/features/orders/repository/orders.repository.d.ts +32 -0
  89. package/dist/features/orders/repository/orders.repository.js +14 -0
  90. package/dist/features/orders/types/index.d.ts +22 -0
  91. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
  92. package/dist/features/products/components/ArtStickersListView.js +5 -4
  93. package/dist/features/products/components/MarketplaceBundleCard.js +2 -2
  94. package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
  95. package/dist/features/products/components/PrizeDrawCollage.js +1 -1
  96. package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
  97. package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
  98. package/dist/features/products/components/ProductDetailPageView.js +1 -1
  99. package/dist/features/products/components/ProductGrid.js +3 -3
  100. package/dist/features/products/components/ProductsIndexListing.js +87 -11
  101. package/dist/features/products/components/ProductsIndexPageView.js +14 -2
  102. package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
  103. package/dist/features/products/config/listing-type-listing-config.js +74 -0
  104. package/dist/features/products/hooks/useProducts.js +14 -0
  105. package/dist/features/products/repository/products.repository.d.ts +0 -4
  106. package/dist/features/products/repository/products.repository.js +0 -15
  107. package/dist/features/products/types/index.d.ts +15 -0
  108. package/dist/features/promotions/repository/coupons.repository.d.ts +12 -0
  109. package/dist/features/promotions/repository/coupons.repository.js +12 -0
  110. package/dist/features/seller/components/SellerOrdersView.js +2 -1
  111. package/dist/features/seller/components/SellerSidebar.js +1 -1
  112. package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
  113. package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
  114. package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
  115. package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
  116. package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
  117. package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
  118. package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
  119. package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
  120. package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
  121. package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
  122. package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
  123. package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
  124. package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
  125. package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
  126. package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
  127. package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
  128. package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
  129. package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
  130. package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
  131. package/dist/features/stores/components/StoreProductsListing.js +3 -2
  132. package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
  133. package/dist/features/stores/components/StoreProductsPageView.js +16 -21
  134. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +875 -0
  135. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +1 -1
  136. package/dist/jobs.d.ts +1 -1
  137. package/dist/jobs.js +1 -1
  138. package/dist/next/routing/route-map.d.ts +2 -0
  139. package/dist/next/routing/route-map.js +5 -0
  140. package/dist/seed/homepage-sections-seed-data.js +5 -1
  141. package/dist/seed/reviews-seed-data.js +52 -0
  142. package/dist/seed/site-settings-seed-data.js +18 -0
  143. package/dist/server-entry.d.ts +1 -0
  144. package/dist/server-entry.js +1 -0
  145. package/dist/styles.css +66 -1
  146. package/dist/tailwind-utilities.css +1 -1
  147. package/dist/tokens/color-pairs.js +1 -0
  148. package/dist/tokens/tokens.css +33 -0
  149. package/dist/ui/components/Button.style.css +21 -0
  150. package/dist/ui/components/Checkbox.d.ts +10 -1
  151. package/dist/ui/components/Checkbox.js +14 -2
  152. package/dist/ui/components/Checkbox.style.css +11 -0
  153. package/dist/ui/components/ImageLightbox.js +7 -4
  154. package/dist/ui/components/surface-tokens.d.ts +7 -0
  155. package/dist/ui/components/surface-tokens.js +7 -0
  156. package/package.json +1 -1
  157. package/dist/_internal/server/jobs/core/onBidPlaced.d.ts +0 -15
  158. package/dist/_internal/server/jobs/core/onBidPlaced.js +0 -63
  159. package/dist/_internal/server/jobs/handlers/onBidPlaced.d.ts +0 -3
  160. package/dist/_internal/server/jobs/handlers/onBidPlaced.js +0 -9
@@ -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];
@@ -6,7 +6,6 @@
6
6
  * The path patterns mirror the original `functions/src/index.ts` declarations
7
7
  * to preserve identical deploy behavior.
8
8
  */
9
- export declare const onBidPlaced: import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewBid>;
10
9
  export declare const onOrderCreate: import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewOrder>;
11
10
  export declare const onOrderStatusChange: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").OrderBefore, import("../jobs").OrderAfter>;
12
11
  export declare const onProductWrite: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").ProductDoc, import("../jobs").ProductDoc>;
@@ -51,7 +50,7 @@ export declare const onCatalogueSubmittedForApproval: import("./types").Document
51
50
  [x: string]: import("../../..").JsonValue;
52
51
  }>;
53
52
  export declare const onJobCreated: import("./types").DocumentTriggerFunctionDefinition<null, import("../../../server").JobDocument>;
54
- export declare const FIRESTORE_TRIGGER_FUNCTIONS: readonly [import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewBid>, import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewOrder>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").OrderBefore, import("../jobs").OrderAfter>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").ProductDoc, import("../jobs").ProductDoc>, import("./types").DocumentTriggerFunctionDefinition<{
53
+ export declare const FIRESTORE_TRIGGER_FUNCTIONS: readonly [import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewOrder>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").OrderBefore, import("../jobs").OrderAfter>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").ProductDoc, import("../jobs").ProductDoc>, import("./types").DocumentTriggerFunctionDefinition<{
55
54
  [x: string]: import("../../..").JsonValue;
56
55
  }, {
57
56
  [x: string]: import("../../..").JsonValue;
@@ -6,16 +6,9 @@
6
6
  * The path patterns mirror the original `functions/src/index.ts` declarations
7
7
  * to preserve identical deploy behavior.
8
8
  */
9
- import { onBidPlacedHandler, onCategoryWriteHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onProductStockChangeHandler, onProductWriteHandler, onReviewWriteHandler, onScamReportCreateHandler, onScamReportUpdateHandler, onStoreWriteHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, onShipmentItemWriteHandler, onShipmentLotWriteHandler, onShipmentHeaderWriteHandler, onShipmentDeletedHandler, onCatalogueSubmittedForApprovalHandler, onJobCreatedHandler, onPrizeDrawPaymentConfirmedHandler, prizeDrawSoldOutRevealHandler, } from "../jobs/handlers";
9
+ import { onCategoryWriteHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onProductStockChangeHandler, onProductWriteHandler, onReviewWriteHandler, onScamReportCreateHandler, onScamReportUpdateHandler, onStoreWriteHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, onShipmentItemWriteHandler, onShipmentLotWriteHandler, onShipmentHeaderWriteHandler, onShipmentDeletedHandler, onCatalogueSubmittedForApprovalHandler, onJobCreatedHandler, onPrizeDrawPaymentConfirmedHandler, prizeDrawSoldOutRevealHandler, } from "../jobs/handlers";
10
10
  import { defineFunction } from "./define";
11
11
  const REGION = "asia-south1";
12
- export const onBidPlaced = defineFunction({
13
- name: "onBidPlaced",
14
- description: "Side-effects on bid creation (notifications, outbid emails).",
15
- trigger: { kind: "documentCreated", pathPattern: "bids/{bidId}" },
16
- handler: onBidPlacedHandler,
17
- options: { region: REGION },
18
- });
19
12
  export const onOrderCreate = defineFunction({
20
13
  name: "onOrderCreate",
21
14
  description: "Side-effects on order creation (notify store, decrement stock).",
@@ -164,7 +157,6 @@ export const onJobCreated = defineFunction({
164
157
  options: { region: REGION, timeoutSeconds: 300, memory: "512MiB" },
165
158
  });
166
159
  export const FIRESTORE_TRIGGER_FUNCTIONS = [
167
- onBidPlaced,
168
160
  onOrderCreate,
169
161
  onOrderStatusChange,
170
162
  onProductWrite,
@@ -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) {
@@ -36,7 +36,6 @@ export { runStoreAnalytics, type StoreAnalyticsInput, type StoreAnalyticsResult,
36
36
  export { runAdminAnalytics, type AdminAnalyticsResult } from "./adminAnalytics";
37
37
  export { runTriggerEventRaffle, type TriggerEventRaffleInput, type TriggerEventRaffleResult, } from "./triggerEventRaffle";
38
38
  export { runAssignSpinPrize, type AssignSpinPrizeInput, type AssignSpinPrizeResult, } from "./assignSpinPrize";
39
- export { handleBidPlaced, type HandleBidPlacedInput, type NewBid } from "./onBidPlaced";
40
39
  export { handleOrderCreate, type HandleOrderCreateInput, type NewOrder, } from "./onOrderCreate";
41
40
  export { handleOrderStatusChange, type HandleOrderStatusChangeInput, type OrderAfter, type OrderBefore, } from "./onOrderStatusChange";
42
41
  export { handleProductWrite, type HandleProductWriteInput, type ProductDoc, } from "./onProductWrite";
@@ -39,7 +39,6 @@ export { runAdminAnalytics } from "./adminAnalytics";
39
39
  export { runTriggerEventRaffle, } from "./triggerEventRaffle";
40
40
  export { runAssignSpinPrize, } from "./assignSpinPrize";
41
41
  // Trigger reactors — call with the meaningful payload, no Firestore event needed
42
- export { handleBidPlaced } from "./onBidPlaced";
43
42
  export { handleOrderCreate, } from "./onOrderCreate";
44
43
  export { handleOrderStatusChange, } from "./onOrderStatusChange";
45
44
  export { handleProductWrite, } from "./onProductWrite";
@@ -1,5 +1,5 @@
1
1
  import { normalizeError } from "../../../../errors/normalize";
2
- import { bidRepository, cartRepository, offerRepository } from "../../../../repositories";
2
+ import { bidRepository, cartRepository, offerRepository, storeRepository } from "../../../../repositories";
3
3
  import { sendNotification } from "../../../../features/admin/actions/notification-actions";
4
4
  export async function runOfferExpiry(ctx) {
5
5
  ctx.logger.info("Starting offer expiry sweep");
@@ -141,6 +141,37 @@ async function lapseUnpaidAuctionWins(ctx) {
141
141
  if (auctionLines.length === 0)
142
142
  return;
143
143
  ctx.logger.info(`Found ${auctionLines.length} unpaid auction win(s) past deadline`);
144
+ /**
145
+ * Tell the seller their item is unsold again so they can relist it or approach
146
+ * the runner-up. Only the buyer was ever notified, so to a seller a forfeited
147
+ * win looked like a completed sale that simply never paid out.
148
+ *
149
+ * Best-effort by design: the forfeiture itself (cart line cleared, bid marked
150
+ * forfeited) is already committed by the time this runs, and a missing store or
151
+ * a notification failure must not roll that back or abort the sweep.
152
+ */
153
+ async function notifySellerOfForfeitedWin(item, jobCtx) {
154
+ try {
155
+ const store = item.storeId ? await storeRepository.findById(item.storeId) : null;
156
+ if (!store?.ownerId)
157
+ return;
158
+ await sendNotification({
159
+ userId: store.ownerId,
160
+ type: "auction_ended",
161
+ priority: "normal",
162
+ title: "Auction win forfeited — item unsold",
163
+ message: `The winning bidder for "${item.productTitle}" did not pay within the deadline, so the win was forfeited. You can relist the item or approach the next highest bidder.`,
164
+ relatedId: item.auctionId ?? item.productId,
165
+ relatedType: "product",
166
+ });
167
+ }
168
+ catch (sellerErr) {
169
+ void normalizeError(sellerErr);
170
+ jobCtx.logger.warn(`Failed to notify seller of forfeited win ${item.bidId}`, {
171
+ error: sellerErr instanceof Error ? sellerErr.message : String(sellerErr),
172
+ });
173
+ }
174
+ }
144
175
  for (const { userId, item } of auctionLines) {
145
176
  try {
146
177
  await cartRepository.removeItemsByBidId(userId, item.bidId);
@@ -154,6 +185,7 @@ async function lapseUnpaidAuctionWins(ctx) {
154
185
  relatedId: item.auctionId ?? item.productId,
155
186
  relatedType: "product",
156
187
  });
188
+ await notifySellerOfForfeitedWin(item, ctx);
157
189
  }
158
190
  catch (err) {
159
191
  void normalizeError(err);
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * S4 batch 1: promotions, onOrderCreate, onOrderStatusChange,
6
6
  * auctionSettlement, autoPayoutEligibility, couponExpiry, offerExpiry
7
- * S5 batch 2: onReviewWrite, onBidPlaced, cartPrune, notificationPrune,
7
+ * S5 batch 2: onReviewWrite, cartPrune, notificationPrune,
8
8
  * dailyDataCleanup, countersReconcile, cleanupRtdbEvents
9
9
  */
10
10
  export { couponExpiryHandler } from "./couponExpiry";
@@ -18,7 +18,6 @@ export { autoPayoutEligibilityHandler } from "./autoPayoutEligibility";
18
18
  export { countersReconcileHandler } from "./countersReconcile";
19
19
  export { onOrderCreateHandler } from "./onOrderCreate";
20
20
  export { onOrderStatusChangeHandler } from "./onOrderStatusChange";
21
- export { onBidPlacedHandler } from "./onBidPlaced";
22
21
  export { onJobCreatedHandler } from "./onJobCreated";
23
22
  export { onReviewWriteHandler } from "./onReviewWrite";
24
23
  export { promotionsHandler, type PromotionsCallableResult } from "./promotions";
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * S4 batch 1: promotions, onOrderCreate, onOrderStatusChange,
6
6
  * auctionSettlement, autoPayoutEligibility, couponExpiry, offerExpiry
7
- * S5 batch 2: onReviewWrite, onBidPlaced, cartPrune, notificationPrune,
7
+ * S5 batch 2: onReviewWrite, cartPrune, notificationPrune,
8
8
  * dailyDataCleanup, countersReconcile, cleanupRtdbEvents
9
9
  */
10
10
  export { couponExpiryHandler } from "./couponExpiry";
@@ -18,7 +18,6 @@ export { autoPayoutEligibilityHandler } from "./autoPayoutEligibility";
18
18
  export { countersReconcileHandler } from "./countersReconcile";
19
19
  export { onOrderCreateHandler } from "./onOrderCreate";
20
20
  export { onOrderStatusChangeHandler } from "./onOrderStatusChange";
21
- export { onBidPlacedHandler } from "./onBidPlaced";
22
21
  export { onJobCreatedHandler } from "./onJobCreated";
23
22
  export { onReviewWriteHandler } from "./onReviewWrite";
24
23
  export { promotionsHandler } from "./promotions";
@@ -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;
@@ -24,3 +24,37 @@ export declare function resolveTieredBidIncrement(currentBidAmount: number, tier
24
24
  * MORE than the platform tier, but can never undercut it.
25
25
  */
26
26
  export declare function resolveMinBidIncrement(currentBidAmount: number, tiers: BidIncrementTier[], override?: number | null): number;
27
+ export interface ResolveMinBidOptions {
28
+ /**
29
+ * Whether the auction already has at least one bid. Defaults to `true`.
30
+ *
31
+ * When `false` the starting bid is itself acceptable — a seller's opening
32
+ * price is a price, not a floor that must be beaten. Requiring
33
+ * `startingBid + increment` for the very first bid (which is what this
34
+ * function did unconditionally before) meant a ₹100 auction could never be
35
+ * opened at ₹100, only at ₹110, contradicting both the "Starting bid ₹100"
36
+ * label shown next to the field and the eBay convention buyers expect.
37
+ */
38
+ hasBids?: boolean;
39
+ }
40
+ /**
41
+ * The single definition of "the lowest amount this auction will accept next".
42
+ *
43
+ * Both the bid form and `placeBid` must agree on this exactly, or the form
44
+ * seeds an amount the server then rejects. Deriving it in two places is the
45
+ * drift shape Root Cause #30 describes, so both call this instead.
46
+ *
47
+ * `currentBidAmount` is the auction's live visible price — for an auction with
48
+ * no bids yet, callers pass `startingBid` (mirroring `placeBid`'s `baseBid`)
49
+ * together with `{ hasBids: false }`.
50
+ */
51
+ export declare function resolveMinBid(currentBidAmount: number, tiers: BidIncrementTier[], override?: number | null, opts?: ResolveMinBidOptions): number;
52
+ /**
53
+ * Preset step multipliers offered by the bid form, as multiples of the
54
+ * effective minimum increment. With a ₹100 increment this renders
55
+ * +₹100 / +₹500 / +₹1,000; with a ₹1,000 increment, +₹1,000 / +₹5,000 /
56
+ * +₹10,000. Never hardcode the rupee amounts at the call site — the whole
57
+ * point is that the steps track the tier.
58
+ */
59
+ export declare const BID_PRESET_MULTIPLIERS: readonly [1, 5, 10];
60
+ export type BidPresetMultiplier = (typeof BID_PRESET_MULTIPLIERS)[number];
@@ -12,6 +12,23 @@ export const DEFAULT_AUCTION_BID_INCREMENT_TIERS = [
12
12
  { upTo: 10000, increment: 500 },
13
13
  { upTo: null, increment: 1000 },
14
14
  ];
15
+ /**
16
+ * Drop malformed rows and fall back to the seed table when nothing usable is
17
+ * left.
18
+ *
19
+ * An empty/missing tier array does NOT mean "this auction has no increment
20
+ * rule" — it means the `siteSettings/global` singleton predates
21
+ * `auctionConfig.bidIncrementTiers`, or an admin cleared every row. Every
22
+ * caller reaches this via `settings.auctionConfig?.bidIncrementTiers ?? []`,
23
+ * so before this guard existed that case fell through to the ₹1
24
+ * `AUCTION_MIN_BID_INCREMENT` last-resort constant, which made the effective
25
+ * minimum step ₹1 on every auction on the site — that is what rendered the
26
+ * bid presets as "+₹1 / +₹5 / +₹10" instead of tier-derived steps.
27
+ */
28
+ function usableTiers(tiers) {
29
+ const valid = (tiers ?? []).filter((t) => t && Number.isFinite(t.increment) && t.increment > 0);
30
+ return valid.length > 0 ? valid : DEFAULT_AUCTION_BID_INCREMENT_TIERS;
31
+ }
15
32
  /**
16
33
  * Resolve the tiered minimum bid increment for a given current bid amount.
17
34
  * Tier lookup is inclusive of each band's upper bound — e.g. with the
@@ -19,11 +36,12 @@ export const DEFAULT_AUCTION_BID_INCREMENT_TIERS = [
19
36
  * increment (the 100-1000 tier), not the 200 increment of the next band.
20
37
  */
21
38
  export function resolveTieredBidIncrement(currentBidAmount, tiers) {
22
- for (const tier of tiers) {
39
+ const table = usableTiers(tiers);
40
+ for (const tier of table) {
23
41
  if (tier.upTo === null || currentBidAmount <= tier.upTo)
24
42
  return tier.increment;
25
43
  }
26
- return tiers[tiers.length - 1]?.increment ?? AUCTION_MIN_BID_INCREMENT;
44
+ return table[table.length - 1]?.increment ?? AUCTION_MIN_BID_INCREMENT;
27
45
  }
28
46
  /**
29
47
  * Effective minimum bid increment = max(admin tier floor, per-listing
@@ -32,5 +50,31 @@ export function resolveTieredBidIncrement(currentBidAmount, tiers) {
32
50
  */
33
51
  export function resolveMinBidIncrement(currentBidAmount, tiers, override) {
34
52
  const tierValue = resolveTieredBidIncrement(currentBidAmount, tiers);
35
- return typeof override === "number" && override > tierValue ? override : tierValue;
53
+ return typeof override === "number" && Number.isFinite(override) && override > tierValue
54
+ ? override
55
+ : tierValue;
56
+ }
57
+ /**
58
+ * The single definition of "the lowest amount this auction will accept next".
59
+ *
60
+ * Both the bid form and `placeBid` must agree on this exactly, or the form
61
+ * seeds an amount the server then rejects. Deriving it in two places is the
62
+ * drift shape Root Cause #30 describes, so both call this instead.
63
+ *
64
+ * `currentBidAmount` is the auction's live visible price — for an auction with
65
+ * no bids yet, callers pass `startingBid` (mirroring `placeBid`'s `baseBid`)
66
+ * together with `{ hasBids: false }`.
67
+ */
68
+ export function resolveMinBid(currentBidAmount, tiers, override, opts) {
69
+ if (opts?.hasBids === false)
70
+ return currentBidAmount;
71
+ return currentBidAmount + resolveMinBidIncrement(currentBidAmount, tiers, override);
36
72
  }
73
+ /**
74
+ * Preset step multipliers offered by the bid form, as multiples of the
75
+ * effective minimum increment. With a ₹100 increment this renders
76
+ * +₹100 / +₹500 / +₹1,000; with a ₹1,000 increment, +₹1,000 / +₹5,000 /
77
+ * +₹10,000. Never hardcode the rupee amounts at the call site — the whole
78
+ * point is that the steps track the tier.
79
+ */
80
+ export const BID_PRESET_MULTIPLIERS = [1, 5, 10];
@@ -1,4 +1,8 @@
1
1
  export declare const CHECKOUT_DEFAULT_COMMISSIONS: {
2
+ readonly platformFeePercent: 5;
3
+ readonly gstPercent: 18;
4
+ readonly minimumTransactionFee: 0;
5
+ readonly platformFeeMax: 10;
2
6
  readonly codDepositPercent: 10;
3
7
  readonly codHandlingFeeMin: 200;
4
8
  readonly codHandlingFeePercent: 10;
@@ -1,4 +1,12 @@
1
1
  export const CHECKOUT_DEFAULT_COMMISSIONS = {
2
+ // Platform-commission rates. Previously absent here because no checkout path
3
+ // computed a buyer-facing platform fee — the moment one did, a siteSettings
4
+ // document that failed to load would have produced NaN fees rather than a
5
+ // sane fallback. Mirrors the SiteSettingsDocument defaults.
6
+ platformFeePercent: 5,
7
+ gstPercent: 18,
8
+ minimumTransactionFee: 0,
9
+ platformFeeMax: 10,
2
10
  codDepositPercent: 10,
3
11
  codHandlingFeeMin: 200,
4
12
  codHandlingFeePercent: 10,
@@ -18,17 +18,49 @@ export interface FeeCommissionRates {
18
18
  platformFeePercent: number;
19
19
  gstPercent: number;
20
20
  /** Rupee floor (not paise) — matches the existing `minimumTransactionFee` doc convention. */ minimumTransactionFee?: number;
21
+ /**
22
+ * Rupee ceiling on the buyer-facing platform commission. Optional so
23
+ * siteSettings documents saved before this field existed keep working —
24
+ * falls back to DEFAULT_PLATFORM_FEE_MAX (₹10).
25
+ *
26
+ * Applies to `computeCheckoutFees` (what the BUYER pays) only. The seller-side
27
+ * `computePayoutDeduction` is deliberately uncapped — that is a separate
28
+ * commercial decision, not the same number.
29
+ */ platformFeeMax?: number;
21
30
  gatewayFeePercent?: number;
22
31
  }
23
32
  export interface CheckoutFees {
24
- /** Platform commission in decimal rupees. */
33
+ /** Platform commission in decimal rupees, capped at `platformFeeMax`. */
25
34
  platformFee: number;
26
- /** GST on the platform fee, in decimal rupees. */
35
+ /** GST on the (already capped) platform fee, in decimal rupees. */
27
36
  gstOnFee: number;
28
37
  /** platformFee + gstOnFee, floored at minimumTransactionFee. */
29
38
  totalFee: number;
30
39
  }
40
+ /**
41
+ * What the buyer pays on top of the subtotal.
42
+ *
43
+ * The cap is applied to the commission BEFORE GST is computed, because GST is
44
+ * levied on the commission actually charged — capping afterwards would bill tax
45
+ * on money that was never collected.
46
+ */
31
47
  export declare function computeCheckoutFees(subtotal: number, commissions: FeeCommissionRates): CheckoutFees;
48
+ /**
49
+ * Split one checkout's platform commission across the orders it produces.
50
+ *
51
+ * The buyer is charged a single capped commission for the transaction, but the
52
+ * cart splits into one order per store, and each `OrderDocument` records its own
53
+ * `platformFee` (invoices, reconciliation). Apportioning pro-rata by group
54
+ * subtotal keeps those per-order figures meaningful; giving the last group the
55
+ * rounding remainder guarantees they sum to exactly what was charged, so the
56
+ * orders can never quietly total a rupee more or less than the buyer paid.
57
+ *
58
+ * @param groupSubtotals `[storeId, subtotal]` pairs, in stable order.
59
+ */
60
+ export declare function allocateCheckoutFees(groupSubtotals: readonly (readonly [string, number])[], fees: CheckoutFees): Map<string, {
61
+ platformFee: number;
62
+ gstOnFee: number;
63
+ }>;
32
64
  export interface PayoutDeduction {
33
65
  /** Platform commission in decimal rupees. */
34
66
  platformFee: number;
@@ -15,12 +15,56 @@
15
15
  * All money amounts are in decimal rupees.
16
16
  */
17
17
  import { roundRupees } from "../../../utils/number.formatter";
18
+ /** Buyer-facing platform commission ceiling, in decimal rupees. */
19
+ const DEFAULT_PLATFORM_FEE_MAX = 10;
20
+ /**
21
+ * What the buyer pays on top of the subtotal.
22
+ *
23
+ * The cap is applied to the commission BEFORE GST is computed, because GST is
24
+ * levied on the commission actually charged — capping afterwards would bill tax
25
+ * on money that was never collected.
26
+ */
18
27
  export function computeCheckoutFees(subtotal, commissions) {
19
- const platformFee = roundRupees(subtotal * (commissions.platformFeePercent / 100));
28
+ const uncapped = roundRupees(subtotal * (commissions.platformFeePercent / 100));
29
+ const cap = commissions.platformFeeMax ?? DEFAULT_PLATFORM_FEE_MAX;
30
+ const platformFee = Math.min(uncapped, cap);
20
31
  const gstOnFee = roundRupees(platformFee * (commissions.gstPercent / 100));
21
32
  const totalFee = Math.max(platformFee + gstOnFee, commissions.minimumTransactionFee ?? 0);
22
33
  return { platformFee, gstOnFee, totalFee };
23
34
  }
35
+ /**
36
+ * Split one checkout's platform commission across the orders it produces.
37
+ *
38
+ * The buyer is charged a single capped commission for the transaction, but the
39
+ * cart splits into one order per store, and each `OrderDocument` records its own
40
+ * `platformFee` (invoices, reconciliation). Apportioning pro-rata by group
41
+ * subtotal keeps those per-order figures meaningful; giving the last group the
42
+ * rounding remainder guarantees they sum to exactly what was charged, so the
43
+ * orders can never quietly total a rupee more or less than the buyer paid.
44
+ *
45
+ * @param groupSubtotals `[storeId, subtotal]` pairs, in stable order.
46
+ */
47
+ export function allocateCheckoutFees(groupSubtotals, fees) {
48
+ const allocation = new Map();
49
+ if (groupSubtotals.length === 0)
50
+ return allocation;
51
+ const total = groupSubtotals.reduce((sum, [, subtotal]) => sum + subtotal, 0);
52
+ let platformRemaining = fees.platformFee;
53
+ let gstRemaining = fees.gstOnFee;
54
+ groupSubtotals.forEach(([storeId, subtotal], index) => {
55
+ const isLast = index === groupSubtotals.length - 1;
56
+ // An all-zero cart can't be apportioned by share — fall back to giving the
57
+ // whole (necessarily zero-ish) fee to the last group rather than dividing
58
+ // by zero.
59
+ const share = total > 0 ? subtotal / total : 0;
60
+ const platformFee = isLast ? roundRupees(platformRemaining) : roundRupees(fees.platformFee * share);
61
+ const gstOnFee = isLast ? roundRupees(gstRemaining) : roundRupees(fees.gstOnFee * share);
62
+ platformRemaining = roundRupees(platformRemaining - platformFee);
63
+ gstRemaining = roundRupees(gstRemaining - gstOnFee);
64
+ allocation.set(storeId, { platformFee, gstOnFee });
65
+ });
66
+ return allocation;
67
+ }
24
68
  export function computePayoutDeduction(grossAmount, commissions) {
25
69
  const platformFee = roundRupees(grossAmount * (commissions.platformFeePercent / 100));
26
70
  const gatewayFee = roundRupees(grossAmount * ((commissions.gatewayFeePercent ?? 0) / 100));
@@ -9,7 +9,7 @@ export const config = {
9
9
  slugPrefix: "auction-",
10
10
  cartLine: "blocked",
11
11
  detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.AUCTION_DETAIL(idOrSlug)),
12
- badge: { label: "Auction", className: "bg-warning-surface text-white" },
12
+ badge: { label: "Auction", className: "bg-warning-solid text-warning-on-solid" },
13
13
  priceLabel: "Suggested Retail Price (₹)",
14
14
  typeLabel: "Auction",
15
15
  showsStockQuantity: false,
@@ -9,7 +9,7 @@ export const config = {
9
9
  slugPrefix: "digitalcode-",
10
10
  cartLine: "single-product",
11
11
  detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.DIGITAL_CODE_DETAIL(idOrSlug)),
12
- badge: { label: "Digital Code", className: "bg-success-surface text-success" },
12
+ badge: { label: "Digital Code", className: "bg-success-solid text-success-on-solid" },
13
13
  priceLabel: "Price per Code (₹)",
14
14
  typeLabel: "Digital Code",
15
15
  showsStockQuantity: false,
@@ -9,7 +9,7 @@ export const config = {
9
9
  slugPrefix: "live-",
10
10
  cartLine: "single-product",
11
11
  detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.LIVE_DETAIL(idOrSlug)),
12
- badge: { label: "Live Item", className: "bg-danger-surface text-error" },
12
+ badge: { label: "Live Item", className: "bg-error-solid text-error-on-solid" },
13
13
  priceLabel: "Price (₹)",
14
14
  typeLabel: "Live Item",
15
15
  showsStockQuantity: true,
@@ -9,7 +9,7 @@ export const config = {
9
9
  slugPrefix: "preorder-",
10
10
  cartLine: "single-product",
11
11
  detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(idOrSlug)),
12
- badge: { label: "Pre-Order", className: "bg-info-surface text-white" },
12
+ badge: { label: "Pre-Order", className: "bg-info-solid text-info-on-solid" },
13
13
  priceLabel: "Pre-Order Price (₹)",
14
14
  typeLabel: "Pre-Order",
15
15
  showsStockQuantity: false,