@mohasinac/appkit 4.11.2 → 4.11.3

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 (165) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
  2. package/dist/_internal/server/features/checkout/actions.js +111 -4
  3. package/dist/_internal/server/features/checkout/data.d.ts +10 -0
  4. package/dist/_internal/server/features/orders/adapters.js +7 -0
  5. package/dist/_internal/server/features/products/data.d.ts +12 -3
  6. package/dist/_internal/server/features/products/data.js +55 -4
  7. package/dist/_internal/server/features/products/index.d.ts +1 -1
  8. package/dist/_internal/server/features/products/index.js +1 -1
  9. package/dist/_internal/server/features/products/list-public.d.ts +19 -0
  10. package/dist/_internal/server/features/products/list-public.js +39 -6
  11. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  12. package/dist/_internal/server/features/reviews/data.js +3 -2
  13. package/dist/_internal/server/features/tester/visibility.js +3 -3
  14. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
  15. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
  16. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
  17. package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
  18. package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
  19. package/dist/_internal/shared/features/promotions/schema.js +0 -2
  20. package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
  21. package/dist/_internal/shared/features/reviews/config.js +8 -1
  22. package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
  23. package/dist/_internal/shared/listing-types/_registry.js +63 -1
  24. package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
  25. package/dist/_internal/shared/listing-types/art/config.js +12 -0
  26. package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
  27. package/dist/_internal/shared/listing-types/auction/config.js +17 -0
  28. package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
  29. package/dist/_internal/shared/listing-types/classified/config.js +14 -0
  30. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
  31. package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
  32. package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
  33. package/dist/_internal/shared/listing-types/live/config.js +14 -0
  34. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
  35. package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
  36. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
  37. package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
  38. package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
  39. package/dist/_internal/shared/listing-types/standard/config.js +9 -0
  40. package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
  41. package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
  42. package/dist/client.d.ts +5 -1
  43. package/dist/client.js +4 -1
  44. package/dist/constants/field-names.d.ts +12 -0
  45. package/dist/constants/field-names.js +12 -0
  46. package/dist/constants/table-keys.d.ts +2 -0
  47. package/dist/constants/table-keys.js +2 -0
  48. package/dist/features/account/components/UserOrdersView.js +27 -5
  49. package/dist/features/admin/components/AdminAddressesView.js +3 -1
  50. package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
  51. package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
  52. package/dist/features/admin/components/AdminProductsView.js +34 -18
  53. package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
  54. package/dist/features/admin/constants/filter-tabs.js +22 -9
  55. package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
  56. package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
  57. package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
  58. package/dist/features/cart/schemas/firestore.d.ts +0 -2
  59. package/dist/features/categories/components/BrandDetailTabs.js +31 -11
  60. package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
  61. package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
  62. package/dist/features/categories/components/CategoryProductsListing.js +5 -2
  63. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
  64. package/dist/features/contact/email.js +5 -1
  65. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
  66. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  67. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  68. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  69. package/dist/features/layout/AppLayoutShell.js +9 -2
  70. package/dist/features/layout/BottomActions.js +26 -3
  71. package/dist/features/layout/BottomActionsContext.d.ts +14 -0
  72. package/dist/features/layout/BottomActionsContext.js +13 -1
  73. package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
  74. package/dist/features/layout/hooks/useBottomActions.js +4 -1
  75. package/dist/features/layout/index.d.ts +1 -1
  76. package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
  77. package/dist/features/orders/repository/orders.repository.d.ts +4 -0
  78. package/dist/features/orders/repository/orders.repository.js +4 -0
  79. package/dist/features/orders/types/index.d.ts +16 -0
  80. package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
  81. package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
  82. package/dist/features/products/components/AuctionsIndexListing.js +1 -2
  83. package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
  84. package/dist/features/products/components/ListingBottomActions.js +25 -0
  85. package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
  86. package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
  87. package/dist/features/products/components/ProductDetailActions.js +4 -0
  88. package/dist/features/products/components/ProductDetailPageView.js +7 -7
  89. package/dist/features/products/constants/listing-tabs.d.ts +43 -198
  90. package/dist/features/products/constants/listing-tabs.js +71 -58
  91. package/dist/features/products/constants/sieve.d.ts +104 -1
  92. package/dist/features/products/constants/sieve.js +27 -7
  93. package/dist/features/products/repository/products.repository.d.ts +45 -1
  94. package/dist/features/products/repository/products.repository.js +26 -3
  95. package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
  96. package/dist/features/products/utils/listing-badge-variant.js +26 -1
  97. package/dist/features/products/utils/listing-type.d.ts +24 -0
  98. package/dist/features/products/utils/listing-type.js +48 -0
  99. package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
  100. package/dist/features/promotions/actions/coupon-actions.js +12 -1
  101. package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
  102. package/dist/features/promotions/actions/coupon-stacking.js +21 -0
  103. package/dist/features/promotions/actions/index.d.ts +1 -0
  104. package/dist/features/promotions/actions/index.js +1 -0
  105. package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
  106. package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
  107. package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
  108. package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
  109. package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
  110. package/dist/features/promotions/components/index.d.ts +2 -0
  111. package/dist/features/promotions/components/index.js +1 -0
  112. package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
  113. package/dist/features/promotions/constants/coupon-help.js +42 -0
  114. package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
  115. package/dist/features/promotions/repository/coupons.repository.js +22 -8
  116. package/dist/features/promotions/schemas/firestore.d.ts +0 -1
  117. package/dist/features/promotions/schemas/firestore.js +1 -1
  118. package/dist/features/promotions/schemas/index.d.ts +0 -20
  119. package/dist/features/promotions/schemas/index.js +0 -1
  120. package/dist/features/reviews/actions/review-actions.js +5 -1
  121. package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
  122. package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
  123. package/dist/features/reviews/components/ReviewFilters.js +4 -3
  124. package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
  125. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
  126. package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
  127. package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
  128. package/dist/features/reviews/components/index.d.ts +2 -0
  129. package/dist/features/reviews/components/index.js +1 -0
  130. package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
  131. package/dist/features/reviews/hooks/useReviews.js +14 -6
  132. package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
  133. package/dist/features/reviews/repository/reviews.repository.js +77 -41
  134. package/dist/features/reviews/types/index.d.ts +8 -1
  135. package/dist/features/seller/components/SellerProductsCards.js +2 -2
  136. package/dist/features/seller/components/SellerProductsView.js +21 -18
  137. package/dist/features/stores/actions/store-query-actions.js +1 -1
  138. package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
  139. package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
  140. package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
  141. package/dist/features/stores/components/StoreReviewsListing.js +7 -108
  142. package/dist/features/stores/schemas/index.d.ts +2 -2
  143. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
  144. package/dist/features/tester/seed-data/index.d.ts +1 -0
  145. package/dist/features/tester/seed-data/index.js +1 -0
  146. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
  147. package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
  148. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
  149. package/dist/index.d.ts +9 -3
  150. package/dist/index.js +11 -2
  151. package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
  152. package/dist/react/hooks/useListingTypeFlags.js +4 -9
  153. package/dist/schemas/registry.d.ts +0 -12
  154. package/dist/seed/claimed-coupons-seed-data.js +3 -3
  155. package/dist/seed/coupons-seed-data.js +29 -10
  156. package/dist/seed/faq-seed-data.js +2 -2
  157. package/dist/seed/manifest.js +2 -2
  158. package/dist/styles.css +44 -39
  159. package/dist/tailwind-utilities.css +1 -1
  160. package/dist/ui/components/FilterChipGroup.d.ts +21 -3
  161. package/dist/ui/components/FilterChipGroup.js +33 -4
  162. package/dist/ui/components/HorizontalScroller.d.ts +1 -2
  163. package/dist/ui/components/HorizontalScroller.js +9 -5
  164. package/dist/ui/components/HorizontalScroller.style.css +43 -38
  165. package/package.json +1 -1
@@ -109,6 +109,10 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
109
109
  canFilter: boolean;
110
110
  canSort: boolean;
111
111
  };
112
+ isOnSale: {
113
+ canFilter: boolean;
114
+ canSort: boolean;
115
+ };
112
116
  price: {
113
117
  canFilter: boolean;
114
118
  canSort: boolean;
@@ -117,6 +121,10 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
117
121
  canFilter: boolean;
118
122
  canSort: boolean;
119
123
  };
124
+ availableQuantity: {
125
+ canFilter: boolean;
126
+ canSort: boolean;
127
+ };
120
128
  viewCount: {
121
129
  canFilter: boolean;
122
130
  canSort: boolean;
@@ -129,6 +137,10 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
129
137
  canFilter: boolean;
130
138
  canSort: boolean;
131
139
  };
140
+ bidsHaveStarted: {
141
+ canFilter: boolean;
142
+ canSort: boolean;
143
+ };
132
144
  isSold: {
133
145
  canFilter: boolean;
134
146
  canSort: boolean;
@@ -137,6 +149,18 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
137
149
  canFilter: boolean;
138
150
  canSort: boolean;
139
151
  };
152
+ isTestData: {
153
+ canFilter: boolean;
154
+ canSort: boolean;
155
+ };
156
+ brandSlug: {
157
+ canFilter: boolean;
158
+ canSort: boolean;
159
+ };
160
+ sublistingCategoryId: {
161
+ canFilter: boolean;
162
+ canSort: boolean;
163
+ };
140
164
  createdAt: {
141
165
  canFilter: boolean;
142
166
  canSort: boolean;
@@ -166,6 +190,22 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
166
190
  canFilter: boolean;
167
191
  canSort: boolean;
168
192
  };
193
+ prizeDrawMode: {
194
+ canFilter: boolean;
195
+ canSort: boolean;
196
+ };
197
+ pricePerEntry: {
198
+ canFilter: boolean;
199
+ canSort: boolean;
200
+ };
201
+ prizeCurrentEntries: {
202
+ canFilter: boolean;
203
+ canSort: boolean;
204
+ };
205
+ prizeMaxEntries: {
206
+ canFilter: boolean;
207
+ canSort: boolean;
208
+ };
169
209
  startingBid: {
170
210
  canFilter: boolean;
171
211
  canSort: boolean;
@@ -215,6 +255,10 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
215
255
  canFilter: boolean;
216
256
  canSort: boolean;
217
257
  };
258
+ preOrderClosed: {
259
+ canFilter: boolean;
260
+ canSort: boolean;
261
+ };
218
262
  tags: {
219
263
  canFilter: boolean;
220
264
  canSort: boolean;
@@ -231,7 +275,7 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
231
275
  canFilter: boolean;
232
276
  canSort: boolean;
233
277
  };
234
- freeShipping: {
278
+ shippingPaidBy: {
235
279
  canFilter: boolean;
236
280
  canSort: boolean;
237
281
  };
@@ -493,22 +493,38 @@ ProductRepository.SIEVE_FIELDS = {
493
493
  status: { canFilter: true, canSort: true },
494
494
  storeId: { canFilter: true, canSort: false },
495
495
  storeName: { canFilter: true, canSort: true },
496
- featured: { canFilter: true, canSort: false },
496
+ // canSort:true STANDARD_SORT_OPTIONS offers "Featured First" and
497
+ // "Promoted First". They were `canSort:false` until 2026-08-21, so sievejs
498
+ // silently dropped the sort and both dropdown options did nothing on the
499
+ // admin and seller product lists.
500
+ featured: { canFilter: true, canSort: true },
497
501
  listingType: { canFilter: true, canSort: false },
498
- isPromoted: { canFilter: true, canSort: false },
502
+ isPromoted: { canFilter: true, canSort: true },
503
+ isOnSale: { canFilter: true, canSort: false },
499
504
  price: { canFilter: true, canSort: true },
500
505
  stockQuantity: { canFilter: true, canSort: true },
506
+ availableQuantity: { canFilter: true, canSort: true },
501
507
  viewCount: { canFilter: true, canSort: true },
502
508
  currentBid: { canFilter: true, canSort: true },
503
509
  bidCount: { canFilter: true, canSort: true },
510
+ bidsHaveStarted: { canFilter: true, canSort: false },
504
511
  isSold: { canFilter: true, canSort: true },
505
512
  isPartOfBundle: { canFilter: true, canSort: false },
513
+ isTestData: { canFilter: true, canSort: false },
514
+ brandSlug: { canFilter: true, canSort: false },
515
+ sublistingCategoryId: { canFilter: true, canSort: false },
506
516
  createdAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
507
517
  updatedAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
508
518
  auctionEndDate: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
509
519
  prizeRevealWindowStart: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
510
520
  prizeRevealWindowEnd: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
511
521
  prizeRevealStatus: { canFilter: true, canSort: false },
522
+ prizeDrawMode: { canFilter: true, canSort: false },
523
+ // Prize-draw entry price. PrizeDrawsListingView emits min/max filters on
524
+ // this field; without an entry here sievejs dropped both clauses silently.
525
+ pricePerEntry: { canFilter: true, canSort: true },
526
+ prizeCurrentEntries: { canFilter: true, canSort: true },
527
+ prizeMaxEntries: { canFilter: true, canSort: true },
512
528
  startingBid: { canFilter: true, canSort: true },
513
529
  buyNowPrice: { canFilter: true, canSort: true },
514
530
  minBidIncrement: { canFilter: true, canSort: false },
@@ -521,11 +537,18 @@ ProductRepository.SIEVE_FIELDS = {
521
537
  preOrderCurrentCount: { canFilter: true, canSort: true },
522
538
  preOrderProductionStatus: { canFilter: true, canSort: false },
523
539
  preOrderCancellable: { canFilter: true, canSort: false },
540
+ preOrderClosed: { canFilter: true, canSort: false },
524
541
  tags: { canFilter: true, canSort: false },
525
542
  features: { canFilter: true, canSort: false },
526
543
  insurance: { canFilter: true, canSort: false },
527
544
  currency: { canFilter: true, canSort: false },
528
- freeShipping: { canFilter: true, canSort: false },
545
+ // `freeShipping` used to be listed here and is NOT a ProductDocument field
546
+ // at all — free shipping is derived from `shippingPaidBy === "seller"`.
547
+ // The public "Free shipping" toggle emitted `shippingPaidBy==seller`
548
+ // (buildFirestoreSafeFilters), which sievejs then dropped because the real
549
+ // field had no entry: `throwExceptions:false` means an unknown field is
550
+ // silently skipped. The toggle did nothing at all until 2026-08-21.
551
+ shippingPaidBy: { canFilter: true, canSort: false },
529
552
  searchTokens: { canFilter: true, canSort: false },
530
553
  "classified.meetupArea": { canFilter: true, canSort: false },
531
554
  "classified.acceptsShipping": { canFilter: true, canSort: false },
@@ -1,3 +1,5 @@
1
+ import type { ListingType } from "../types/index";
2
+ export type ListingBadgeVariant = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
1
3
  /**
2
4
  * Listing-kind → Badge variant map.
3
5
  *
@@ -6,5 +8,22 @@
6
8
  * deprecated `KIND_BADGE_VARIANT` export that previously lived in
7
9
  * `seller-products-styles.ts` (deleted 2026-06-17 in Phase 3 of the
8
10
  * Three-Layer Style System refactor).
11
+ *
12
+ * Typed `Record<ListingType, …>` (2026-08-21) so a new listing type is a
13
+ * COMPILE error here rather than a silent fallthrough. It previously covered
14
+ * only auction / pre-order / prize-draw / standard, so the other five types
15
+ * fell through to the caller's `?? "default"` and all rendered identically.
16
+ */
17
+ export declare const LISTING_BADGE_VARIANT: Record<ListingType, ListingBadgeVariant>;
18
+ /**
19
+ * Badge variant for a listing kind that may not be a real `ListingType` —
20
+ * a raw API string, or a UI-only sentinel like the seller dropdown's `"all"`.
21
+ *
22
+ * Callers used to index `LISTING_BADGE_VARIANT[kind] ?? "default"` directly
23
+ * with a plain `string`, which is what let the map stay incomplete for years
24
+ * without anyone noticing: the `??` swallowed every missing type. Going
25
+ * through this accessor keeps the map itself exhaustively typed (so a new
26
+ * listing type is a compile error there) while still tolerating a
27
+ * non-ListingType input here, where tolerance is genuinely needed.
9
28
  */
10
- export declare const LISTING_BADGE_VARIANT: Record<string, "default" | "primary" | "secondary" | "success" | "warning" | "danger">;
29
+ export declare function listingBadgeVariant(kind: string | undefined): ListingBadgeVariant;
@@ -1,3 +1,4 @@
1
+ import { isListingType } from "./listing-type";
1
2
  /**
2
3
  * Listing-kind → Badge variant map.
3
4
  *
@@ -6,10 +7,34 @@
6
7
  * deprecated `KIND_BADGE_VARIANT` export that previously lived in
7
8
  * `seller-products-styles.ts` (deleted 2026-06-17 in Phase 3 of the
8
9
  * Three-Layer Style System refactor).
10
+ *
11
+ * Typed `Record<ListingType, …>` (2026-08-21) so a new listing type is a
12
+ * COMPILE error here rather than a silent fallthrough. It previously covered
13
+ * only auction / pre-order / prize-draw / standard, so the other five types
14
+ * fell through to the caller's `?? "default"` and all rendered identically.
9
15
  */
10
16
  export const LISTING_BADGE_VARIANT = {
17
+ standard: "default",
11
18
  auction: "warning",
12
19
  "pre-order": "secondary",
13
20
  "prize-draw": "primary",
14
- standard: "default",
21
+ classified: "secondary",
22
+ "digital-code": "success",
23
+ live: "danger",
24
+ art: "primary",
25
+ stickers: "primary",
15
26
  };
27
+ /**
28
+ * Badge variant for a listing kind that may not be a real `ListingType` —
29
+ * a raw API string, or a UI-only sentinel like the seller dropdown's `"all"`.
30
+ *
31
+ * Callers used to index `LISTING_BADGE_VARIANT[kind] ?? "default"` directly
32
+ * with a plain `string`, which is what let the map stay incomplete for years
33
+ * without anyone noticing: the `??` swallowed every missing type. Going
34
+ * through this accessor keeps the map itself exhaustively typed (so a new
35
+ * listing type is a compile error there) while still tolerating a
36
+ * non-ListingType input here, where tolerance is genuinely needed.
37
+ */
38
+ export function listingBadgeVariant(kind) {
39
+ return kind && isListingType(kind) ? LISTING_BADGE_VARIANT[kind] : "default";
40
+ }
@@ -1,4 +1,28 @@
1
1
  import type { ListingType } from "../types/index";
2
+ /** True when `value` is a member of the canonical `ListingType` union. */
3
+ export declare function isListingType(value: string): value is ListingType;
4
+ /**
5
+ * Parse a multi-select listing-type URL value into canonical types.
6
+ *
7
+ * The type filter is a checkbox group, so its URL value is a pipe-joined set
8
+ * (`auction|pre-order`) — the same shape the Sieve adapter upgrades to a
9
+ * Firestore `in` query. Unknown tokens are DROPPED rather than passed through:
10
+ * an unrecognised value reaching `where("listingType","==",x)` matches zero
11
+ * documents forever with no error (Root Cause #33), so silently ignoring it and
12
+ * showing the wider result set is the safer failure mode.
13
+ *
14
+ * An empty result means "no type filter" — i.e. every type — which is exactly
15
+ * what an empty checkbox group should mean.
16
+ */
17
+ export declare function parseSelectedListingTypes(raw: string | undefined | null): ListingType[];
18
+ /**
19
+ * Toggle one type in a pipe-joined selection, returning the new URL value
20
+ * (`""` when nothing is left selected). Keeps canonical iteration order so the
21
+ * URL is stable regardless of the order the user ticked the boxes — otherwise
22
+ * `auction|art` and `art|auction` would be two different React Query keys for
23
+ * the same result set.
24
+ */
25
+ export declare function toggleListingTypeSelection(raw: string | undefined | null, type: ListingType): string;
2
26
  /**
3
27
  * Canonical accessor for the listing-kind discriminator. `listingType` is the
4
28
  * single source of truth on every `ProductDocument` / `ProductItem` — the
@@ -1,3 +1,51 @@
1
+ import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
2
+ const LISTING_TYPE_SET = new Set(ALL_LISTING_TYPES);
3
+ /** True when `value` is a member of the canonical `ListingType` union. */
4
+ export function isListingType(value) {
5
+ return LISTING_TYPE_SET.has(value);
6
+ }
7
+ /**
8
+ * Parse a multi-select listing-type URL value into canonical types.
9
+ *
10
+ * The type filter is a checkbox group, so its URL value is a pipe-joined set
11
+ * (`auction|pre-order`) — the same shape the Sieve adapter upgrades to a
12
+ * Firestore `in` query. Unknown tokens are DROPPED rather than passed through:
13
+ * an unrecognised value reaching `where("listingType","==",x)` matches zero
14
+ * documents forever with no error (Root Cause #33), so silently ignoring it and
15
+ * showing the wider result set is the safer failure mode.
16
+ *
17
+ * An empty result means "no type filter" — i.e. every type — which is exactly
18
+ * what an empty checkbox group should mean.
19
+ */
20
+ export function parseSelectedListingTypes(raw) {
21
+ if (!raw)
22
+ return [];
23
+ const seen = new Set();
24
+ for (const token of raw.split("|")) {
25
+ const trimmed = token.trim();
26
+ // "All" is the legacy single-select sentinel — treated as "no filter".
27
+ if (!trimmed || trimmed === "All")
28
+ continue;
29
+ if (isListingType(trimmed))
30
+ seen.add(trimmed);
31
+ }
32
+ return [...seen];
33
+ }
34
+ /**
35
+ * Toggle one type in a pipe-joined selection, returning the new URL value
36
+ * (`""` when nothing is left selected). Keeps canonical iteration order so the
37
+ * URL is stable regardless of the order the user ticked the boxes — otherwise
38
+ * `auction|art` and `art|auction` would be two different React Query keys for
39
+ * the same result set.
40
+ */
41
+ export function toggleListingTypeSelection(raw, type) {
42
+ const current = new Set(parseSelectedListingTypes(raw));
43
+ if (current.has(type))
44
+ current.delete(type);
45
+ else
46
+ current.add(type);
47
+ return ALL_LISTING_TYPES.filter((t) => current.has(t)).join("|");
48
+ }
1
49
  /**
2
50
  * Canonical accessor for the listing-kind discriminator. `listingType` is the
3
51
  * single source of truth on every `ProductDocument` / `ProductItem` — the
@@ -26,4 +26,11 @@ export type CouponCartItem = {
26
26
  listingType: ListingType;
27
27
  };
28
28
  export declare function validateCoupon(userId: string, code: string, orderTotal: number): Promise<CouponValidationResult>;
29
+ /**
30
+ * Resolves productId → categorySlugs[] in a single batched `getAll` round-trip.
31
+ * Passed into the coupon repository so it can enforce category restrictions
32
+ * without importing the products repository itself. Invoked only when the
33
+ * coupon actually has a category restriction.
34
+ */
35
+ export declare function resolveCouponCategorySlugs(productIds: string[]): Promise<Map<string, string[]>>;
29
36
  export declare function validateCouponForCart(userId: string, code: string, cartItems: CouponCartItem[]): Promise<CouponCartValidationResult>;
@@ -5,11 +5,22 @@
5
5
  * are handled by the calling server action in the consumer.
6
6
  */
7
7
  import { couponsRepository } from "../repository/coupons.repository";
8
+ import { productRepository } from "../../products/repository/products.repository";
8
9
  export async function validateCoupon(userId, code, orderTotal) {
9
10
  return couponsRepository.validateCoupon(code, userId, orderTotal);
10
11
  }
12
+ /**
13
+ * Resolves productId → categorySlugs[] in a single batched `getAll` round-trip.
14
+ * Passed into the coupon repository so it can enforce category restrictions
15
+ * without importing the products repository itself. Invoked only when the
16
+ * coupon actually has a category restriction.
17
+ */
18
+ export async function resolveCouponCategorySlugs(productIds) {
19
+ const products = await productRepository.listByIds(productIds);
20
+ return new Map(products.map((p) => [p.id, p.categorySlugs ?? []]));
21
+ }
11
22
  export async function validateCouponForCart(userId, code, cartItems) {
12
- const result = await couponsRepository.validateCouponForCart(code, userId, cartItems);
23
+ const result = await couponsRepository.validateCouponForCart(code, userId, cartItems, { resolveCategorySlugs: resolveCouponCategorySlugs });
13
24
  return {
14
25
  valid: result.valid,
15
26
  discountAmount: result.discountAmount ?? 0,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Coupon Stacking Rules
3
+ *
4
+ * A cart may hold at most one seller-scoped coupon per store, plus at most
5
+ * one admin (platform-wide) coupon on top. Both scopes stack with each
6
+ * other freely — the old `combineWithSellerCoupons` opt-out was removed
7
+ * (2026-08-21): stacking is now unconditional as long as the two "buckets"
8
+ * (per-store, and the single platform-wide slot) don't collide.
9
+ *
10
+ * Pure function, no I/O — safe to call from both the cart-apply route and
11
+ * the checkout re-validation pass.
12
+ */
13
+ import type { CartAppliedCoupon } from "../../cart/schemas/firestore";
14
+ export interface IncomingCoupon {
15
+ code: string;
16
+ scope: "admin" | "seller";
17
+ storeId?: string;
18
+ }
19
+ /**
20
+ * Returns a human-readable conflict message, or null when `incoming` can be
21
+ * added alongside `existing` without violating the one-per-store +
22
+ * one-global rule.
23
+ */
24
+ export declare function detectCouponConflict(existing: CartAppliedCoupon[], incoming: IncomingCoupon): string | null;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Returns a human-readable conflict message, or null when `incoming` can be
3
+ * added alongside `existing` without violating the one-per-store +
4
+ * one-global rule.
5
+ */
6
+ export function detectCouponConflict(existing, incoming) {
7
+ for (const applied of existing) {
8
+ if (applied.code === incoming.code) {
9
+ return `Coupon ${incoming.code} is already applied.`;
10
+ }
11
+ if (incoming.scope === "seller" &&
12
+ applied.scope === "seller" &&
13
+ applied.storeId === incoming.storeId) {
14
+ return `A coupon for this store is already applied (${applied.code}). Remove it first.`;
15
+ }
16
+ if (incoming.scope === "admin" && applied.scope === "admin") {
17
+ return `Only one platform-wide coupon can be applied at a time (${applied.code}). Remove it first.`;
18
+ }
19
+ }
20
+ return null;
21
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./coupon-actions";
2
+ export * from "./coupon-stacking";
2
3
  export * from "./promotions-actions";
3
4
  export * from "./seller-coupon-actions";
@@ -1,3 +1,4 @@
1
1
  export * from "./coupon-actions";
2
+ export * from "./coupon-stacking";
2
3
  export * from "./promotions-actions";
3
4
  export * from "./seller-coupon-actions";
@@ -32,7 +32,6 @@ export interface SellerCreateCouponInput {
32
32
  excludeProducts?: string[];
33
33
  excludeCategories?: string[];
34
34
  firstTimeUserOnly: boolean;
35
- combineWithSellerCoupons: boolean;
36
35
  };
37
36
  }
38
37
  export type SellerUpdateCouponInput = Partial<Omit<SellerCreateCouponInput, "sellerCode" | "applicableToAuctions">>;
@@ -36,7 +36,6 @@ export async function sellerCreateCoupon(userId, input) {
36
36
  restrictions: {
37
37
  ...rest.restrictions,
38
38
  applicableSellers: [userId],
39
- combineWithSellerCoupons: false,
40
39
  },
41
40
  stats: { totalUses: 0, totalRevenue: 0, totalDiscount: 0 },
42
41
  });
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ export interface CouponHelpDetailsProps {
3
+ /**
4
+ * Show the note explaining that coupons are re-checked at order placement.
5
+ * Only meaningful where an order is actually about to be placed, so it is
6
+ * off by default and enabled at checkout.
7
+ */
8
+ showRevalidationNote?: boolean;
9
+ defaultOpen?: boolean;
10
+ /** Collapses sibling <Details> sharing the same name (browser-native). */
11
+ name?: string;
12
+ }
13
+ /**
14
+ * Expandable "How coupons work" explainer. Collapsed by default so it never
15
+ * competes with the coupon input it sits beside.
16
+ *
17
+ * All copy comes from COUPON_HELP so the cart, checkout and public coupons
18
+ * listing render identical text.
19
+ */
20
+ export declare function CouponHelpDetails({ showRevalidationNote, defaultOpen, name, }: CouponHelpDetailsProps): React.JSX.Element;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Details, Summary, Stack, Text, Ul, Li, Span } from "../../../ui";
4
+ import { COUPON_HELP } from "../constants/coupon-help";
5
+ function HelpSection({ heading, items }) {
6
+ return (_jsxs(Stack, { gap: "dense", children: [_jsx(Text, { size: "sm", weight: "semibold", children: heading }), _jsx(Ul, { marker: "disc", indent: "md", spacing: "tight", size: "sm", children: items.map((item) => (_jsx(Li, { color: "muted", children: item }, item))) })] }));
7
+ }
8
+ /**
9
+ * Expandable "How coupons work" explainer. Collapsed by default so it never
10
+ * competes with the coupon input it sits beside.
11
+ *
12
+ * All copy comes from COUPON_HELP so the cart, checkout and public coupons
13
+ * listing render identical text.
14
+ */
15
+ export function CouponHelpDetails({ showRevalidationNote = false, defaultOpen = false, name, }) {
16
+ return (_jsxs(Details, { tone: "card", padding: "none", defaultOpen: defaultOpen, name: name, children: [_jsxs(Summary, { paddingX: "x-md", paddingY: "y-md", size: "sm", weight: "semibold", layout: "flex", align: "center", justify: "between", children: [_jsx(Span, { children: COUPON_HELP.title }), _jsx(Span, { size: "xs", color: "muted", children: "Stacking rules" })] }), _jsxs(Stack, { gap: "comfortable", padding: "md", children: [_jsx(HelpSection, { heading: COUPON_HELP.stackingHeading, items: COUPON_HELP.stackingRules }), _jsx(HelpSection, { heading: COUPON_HELP.applicationHeading, items: COUPON_HELP.applicationRules }), _jsx(HelpSection, { heading: COUPON_HELP.failureHeading, items: COUPON_HELP.failureReasons }), showRevalidationNote && (_jsx(Text, { size: "xs", color: "muted", children: COUPON_HELP.revalidationNote }))] })] }));
17
+ }
@@ -6,6 +6,7 @@ import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { Button, DateInput, IconButton, Input, ListingFilterDrawer, Pagination, RadioGroup, SortDropdown, Div, Grid, Row, Span, Stack, Text, Heading, StickyToolbar } from "../../../ui";
7
7
  import { usePromotions } from "../hooks/usePromotions";
8
8
  import { CouponCard } from "./CouponCard";
9
+ import { CouponHelpDetails } from "./CouponHelpDetails";
9
10
  import { TABLE_KEYS } from "../../../constants/table-keys";
10
11
  import { COUPON_FIELDS } from "../../../constants/field-names";
11
12
  import { sortBy } from "../../../constants/sort";
@@ -106,7 +107,7 @@ export function CouponsIndexListing({ initialCoupons, storeId, }) {
106
107
  const clearFilters = () => {
107
108
  table.setMany({ type: "", [TABLE_KEYS.DATE_FROM]: "", [TABLE_KEYS.DATE_TO]: "" });
108
109
  };
109
- return (_jsxs(Div, { className: "min-h-[40vh]", children: [_jsxs(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", z: "above-toolbar", dismissible: true, id: "coupons-toolbar", children: [_jsxs(Row, { gap: "xs", className: "max-w-full", children: [_jsxs(Button, { type: "button", variant: hasActiveFilters ? "outline" : "ghost", onClick: openFilters, gap: "sm", rounded: "lg", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: hasActiveFilters ? "border-primary bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300 shrink-0" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] shrink-0", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsxs(Span, { className: "hidden sm:inline", children: ["Filters", hasActiveFilters ? " •" : ""] })] }), _jsxs(Row, { surface: "default", className: `flex-1 ${__O.hidden}`, border: "strong", rounded: "lg", children: [_jsx(Input, { bare: true, type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), onKeyDown: handleKeyDown, placeholder: "Search by name or description\u2026", className: "min-w-0 flex-1 bg-transparent px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text)] placeholder-zinc-400 outline-none" }), searchInput && (_jsx(IconButton, { type: "button", onClick: () => { setSearchInput(""); table.set(TABLE_KEYS.QUERY, ""); }, "aria-label": "Clear search", variant: "ghost", size: "sm", children: _jsx(X, { className: "h-3.5 w-3.5" }) })), _jsx(IconButton, { type: "button", onClick: commitSearch, "aria-label": "Search", variant: "ghost", size: "sm", children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs(Row, { color: "muted", textSize: "sm", gap: "xs", className: "shrink-0", children: [_jsx(Span, { className: "hidden md:inline whitespace-nowrap", children: "Sort by" }), _jsx(SortDropdown, { value: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, onChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, options: COUPON_SORT_OPTIONS })] })] }), hasActiveFilters && (_jsxs(Row, { gap: "xs", wrap: true, className: "mt-2", children: [activeType && (_jsxs(Span, { layout: "flex", gap: "xs", size: "xs", weight: "medium", className: "bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", padding: "pill-sm-tall", children: [COUPON_TYPES.find((t) => t.value === activeType)?.label ?? activeType, _jsx(Button, { variant: "ghost", type: "button", onClick: () => { table.set(TABLE_KEYS.TYPE, ""); }, "aria-label": "Remove type filter", className: CLS_CHIP_BTN, children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get(TABLE_KEYS.DATE_FROM) && (_jsxs(Span, { layout: "flex", gap: "xs", size: "xs", weight: "medium", className: "bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", padding: "pill-sm-tall", children: ["From: ", table.get(TABLE_KEYS.DATE_FROM), _jsx(Button, { variant: "ghost", type: "button", onClick: () => { table.set(TABLE_KEYS.DATE_FROM, ""); }, "aria-label": "Remove from-date filter", className: CLS_CHIP_BTN, children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get(TABLE_KEYS.DATE_TO) && (_jsxs(Span, { layout: "flex", gap: "xs", size: "xs", weight: "medium", className: "bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", padding: "pill-sm-tall", children: ["To: ", table.get(TABLE_KEYS.DATE_TO), _jsx(Button, { variant: "ghost", type: "button", onClick: () => { table.set(TABLE_KEYS.DATE_TO, ""); }, "aria-label": "Remove to-date filter", className: CLS_CHIP_BTN, children: _jsx(X, { className: "h-3 w-3" }) })] })), _jsx(Button, { variant: "ghost", type: "button", onClick: clearFilters, textSize: "xs", textColor: "muted", className: "underline", children: "Clear all" })] }))] }), _jsxs(Div, { paddingY: "y-lg", paddingX: "x-md", children: [isLoading ? (_jsx(Grid, { gap: "sm", className: "md:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Stack, { border: "skeleton", gap: "sm", rounded: "xl", padding: "md", className: "animate-pulse", children: [_jsx(Div, { className: "h-6 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" })] }, i))) })) : displayCoupons.length === 0 ? (_jsx(Div, { className: "text-left", padding: "y-4xl", children: _jsx(Text, { color: "faint", children: "No coupons match your search." }) })) : (_jsx(Grid, { gap: "sm", className: "md:grid-cols-2 lg:grid-cols-3", children: displayCoupons.map((coupon) => (_jsx(CouponCard, { coupon: coupon, labels: {
110
+ return (_jsxs(Div, { className: "min-h-[40vh]", children: [_jsxs(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", z: "above-toolbar", dismissible: true, id: "coupons-toolbar", children: [_jsxs(Row, { gap: "xs", className: "max-w-full", children: [_jsxs(Button, { type: "button", variant: hasActiveFilters ? "outline" : "ghost", onClick: openFilters, gap: "sm", rounded: "lg", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: hasActiveFilters ? "border-primary bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300 shrink-0" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] shrink-0", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsxs(Span, { className: "hidden sm:inline", children: ["Filters", hasActiveFilters ? " •" : ""] })] }), _jsxs(Row, { surface: "default", className: `flex-1 ${__O.hidden}`, border: "strong", rounded: "lg", children: [_jsx(Input, { bare: true, type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), onKeyDown: handleKeyDown, placeholder: "Search by name or description\u2026", className: "min-w-0 flex-1 bg-transparent px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text)] placeholder-zinc-400 outline-none" }), searchInput && (_jsx(IconButton, { type: "button", onClick: () => { setSearchInput(""); table.set(TABLE_KEYS.QUERY, ""); }, "aria-label": "Clear search", variant: "ghost", size: "sm", children: _jsx(X, { className: "h-3.5 w-3.5" }) })), _jsx(IconButton, { type: "button", onClick: commitSearch, "aria-label": "Search", variant: "ghost", size: "sm", children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs(Row, { color: "muted", textSize: "sm", gap: "xs", className: "shrink-0", children: [_jsx(Span, { className: "hidden md:inline whitespace-nowrap", children: "Sort by" }), _jsx(SortDropdown, { value: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, onChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, options: COUPON_SORT_OPTIONS })] })] }), hasActiveFilters && (_jsxs(Row, { gap: "xs", wrap: true, className: "mt-2", children: [activeType && (_jsxs(Span, { layout: "flex", gap: "xs", size: "xs", weight: "medium", className: "bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", padding: "pill-sm-tall", children: [COUPON_TYPES.find((t) => t.value === activeType)?.label ?? activeType, _jsx(Button, { variant: "ghost", type: "button", onClick: () => { table.set(TABLE_KEYS.TYPE, ""); }, "aria-label": "Remove type filter", className: CLS_CHIP_BTN, children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get(TABLE_KEYS.DATE_FROM) && (_jsxs(Span, { layout: "flex", gap: "xs", size: "xs", weight: "medium", className: "bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", padding: "pill-sm-tall", children: ["From: ", table.get(TABLE_KEYS.DATE_FROM), _jsx(Button, { variant: "ghost", type: "button", onClick: () => { table.set(TABLE_KEYS.DATE_FROM, ""); }, "aria-label": "Remove from-date filter", className: CLS_CHIP_BTN, children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get(TABLE_KEYS.DATE_TO) && (_jsxs(Span, { layout: "flex", gap: "xs", size: "xs", weight: "medium", className: "bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", padding: "pill-sm-tall", children: ["To: ", table.get(TABLE_KEYS.DATE_TO), _jsx(Button, { variant: "ghost", type: "button", onClick: () => { table.set(TABLE_KEYS.DATE_TO, ""); }, "aria-label": "Remove to-date filter", className: CLS_CHIP_BTN, children: _jsx(X, { className: "h-3 w-3" }) })] })), _jsx(Button, { variant: "ghost", type: "button", onClick: clearFilters, textSize: "xs", textColor: "muted", className: "underline", children: "Clear all" })] }))] }), _jsx(Div, { paddingY: "y-md", paddingX: "x-md", children: _jsx(CouponHelpDetails, {}) }), _jsxs(Div, { paddingY: "y-lg", paddingX: "x-md", children: [isLoading ? (_jsx(Grid, { gap: "sm", className: "md:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Stack, { border: "skeleton", gap: "sm", rounded: "xl", padding: "md", className: "animate-pulse", children: [_jsx(Div, { className: "h-6 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" })] }, i))) })) : displayCoupons.length === 0 ? (_jsx(Div, { className: "text-left", padding: "y-4xl", children: _jsx(Text, { color: "faint", children: "No coupons match your search." }) })) : (_jsx(Grid, { gap: "sm", className: "md:grid-cols-2 lg:grid-cols-3", children: displayCoupons.map((coupon) => (_jsx(CouponCard, { coupon: coupon, labels: {
110
111
  copy: "Copy",
111
112
  copied: "Copied!",
112
113
  expires: "Expires",
@@ -1,4 +1,6 @@
1
1
  export { CouponCard } from "./CouponCard";
2
+ export { CouponHelpDetails } from "./CouponHelpDetails";
3
+ export type { CouponHelpDetailsProps } from "./CouponHelpDetails";
2
4
  export { CouponsIndexListing } from "./CouponsIndexListing";
3
5
  export type { CouponsIndexListingProps } from "./CouponsIndexListing";
4
6
  export { PromotionsView, PromotionsViewProductSection, PromotionsHero, } from "./PromotionsView";
@@ -1,3 +1,4 @@
1
1
  export { CouponCard } from "./CouponCard";
2
+ export { CouponHelpDetails } from "./CouponHelpDetails";
2
3
  export { CouponsIndexListing } from "./CouponsIndexListing";
3
4
  export { PromotionsView, PromotionsViewProductSection, PromotionsHero, } from "./PromotionsView";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Coupon Help Copy
3
+ *
4
+ * Single source of truth for the buyer-facing explanation of how coupon
5
+ * scoping and stacking work. Rendered by <CouponHelpDetails> on the cart,
6
+ * the checkout coupon box, and the public coupons listing — so the three
7
+ * surfaces can never drift (same reasoning as REFUND_COPY).
8
+ *
9
+ * Keep this in sync with `detectCouponConflict()`
10
+ * (features/promotions/actions/coupon-stacking.ts), which is what actually
11
+ * enforces the stacking rules described here, and with the seeded FAQ
12
+ * `faq-coupons-and-discounts`.
13
+ */
14
+ export declare const COUPON_HELP: {
15
+ readonly title: "How coupons work";
16
+ /** The stacking rules — the part buyers most often get wrong. */
17
+ readonly stackingHeading: "You can use more than one coupon";
18
+ readonly stackingRules: readonly ["One store coupon per store. If your cart has items from three stores, you can apply each of those stores' coupons at the same time.", "Plus one platform-wide LetItRip coupon, on top of any store coupons.", "A second coupon for the same store, or a second platform-wide coupon, will be declined — remove the first one to swap it."];
19
+ /** How the discount is actually distributed. */
20
+ readonly applicationHeading: "Where each discount lands";
21
+ readonly applicationRules: readonly ["A store coupon only discounts that store's items.", "A platform-wide coupon is spread proportionally across every store in your cart.", "Orders are placed per store, so each store's order shows its own share of the discount."];
22
+ /** Reuses the failure reasons already written for the buyer shopping guide. */
23
+ readonly failureHeading: "Why a coupon might not apply";
24
+ readonly failureReasons: readonly ["The eligible items in your cart are below the coupon's minimum purchase amount — minimum spend is measured against the items the coupon actually covers, not your whole cart.", "The coupon has expired, or hasn't started yet.", "The coupon is limited to certain products or categories, and your cart has none of them.", "The coupon is first-order-only and you've ordered before.", "You've already used the coupon the maximum number of times allowed.", "The coupon ran out — some coupons have a total limit across all buyers."];
25
+ /** Shown at checkout only, where a coupon can be dropped at payment time. */
26
+ readonly revalidationNote: "Coupons are re-checked when you place the order. If one has expired or run out while it sat in your cart, it's removed and your total is recalculated before payment.";
27
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Coupon Help Copy
3
+ *
4
+ * Single source of truth for the buyer-facing explanation of how coupon
5
+ * scoping and stacking work. Rendered by <CouponHelpDetails> on the cart,
6
+ * the checkout coupon box, and the public coupons listing — so the three
7
+ * surfaces can never drift (same reasoning as REFUND_COPY).
8
+ *
9
+ * Keep this in sync with `detectCouponConflict()`
10
+ * (features/promotions/actions/coupon-stacking.ts), which is what actually
11
+ * enforces the stacking rules described here, and with the seeded FAQ
12
+ * `faq-coupons-and-discounts`.
13
+ */
14
+ export const COUPON_HELP = {
15
+ title: "How coupons work",
16
+ /** The stacking rules — the part buyers most often get wrong. */
17
+ stackingHeading: "You can use more than one coupon",
18
+ stackingRules: [
19
+ "One store coupon per store. If your cart has items from three stores, you can apply each of those stores' coupons at the same time.",
20
+ "Plus one platform-wide LetItRip coupon, on top of any store coupons.",
21
+ "A second coupon for the same store, or a second platform-wide coupon, will be declined — remove the first one to swap it.",
22
+ ],
23
+ /** How the discount is actually distributed. */
24
+ applicationHeading: "Where each discount lands",
25
+ applicationRules: [
26
+ "A store coupon only discounts that store's items.",
27
+ "A platform-wide coupon is spread proportionally across every store in your cart.",
28
+ "Orders are placed per store, so each store's order shows its own share of the discount.",
29
+ ],
30
+ /** Reuses the failure reasons already written for the buyer shopping guide. */
31
+ failureHeading: "Why a coupon might not apply",
32
+ failureReasons: [
33
+ "The eligible items in your cart are below the coupon's minimum purchase amount — minimum spend is measured against the items the coupon actually covers, not your whole cart.",
34
+ "The coupon has expired, or hasn't started yet.",
35
+ "The coupon is limited to certain products or categories, and your cart has none of them.",
36
+ "The coupon is first-order-only and you've ordered before.",
37
+ "You've already used the coupon the maximum number of times allowed.",
38
+ "The coupon ran out — some coupons have a total limit across all buyers.",
39
+ ],
40
+ /** Shown at checkout only, where a coupon can be dropped at payment time. */
41
+ revalidationNote: "Coupons are re-checked when you place the order. If one has expired or run out while it sat in your cart, it's removed and your total is recalculated before payment.",
42
+ };
@@ -143,7 +143,16 @@ export declare class CouponsRepository extends BaseRepository<CouponDocument> {
143
143
  quantity: number;
144
144
  /** Canonical listing-kind snapshot (SB1-G Phase 4). */
145
145
  listingType: ListingType;
146
- }>): Promise<{
146
+ }>, opts?: {
147
+ /**
148
+ * Lazily resolves productId → categorySlugs[]. Supplied by the action
149
+ * layer (which may import the products repository) so this repository
150
+ * never takes a cross-repository dependency. Called at most once, and
151
+ * only when the coupon actually carries a category restriction — so a
152
+ * coupon without one costs zero extra Firestore reads.
153
+ */
154
+ resolveCategorySlugs?: (productIds: string[]) => Promise<Map<string, string[]>>;
155
+ }): Promise<{
147
156
  valid: boolean;
148
157
  coupon?: CouponDocument;
149
158
  discountAmount?: number;