@mohasinac/appkit 4.11.1 → 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 (212) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
  2. package/dist/_internal/server/features/checkout/actions.js +138 -27
  3. package/dist/_internal/server/features/checkout/data.d.ts +10 -0
  4. package/dist/_internal/server/features/checkout/locked-lines.d.ts +30 -0
  5. package/dist/_internal/server/features/checkout/locked-lines.js +116 -0
  6. package/dist/_internal/server/features/orders/adapters.js +7 -0
  7. package/dist/_internal/server/features/products/data.d.ts +12 -3
  8. package/dist/_internal/server/features/products/data.js +55 -4
  9. package/dist/_internal/server/features/products/index.d.ts +2 -1
  10. package/dist/_internal/server/features/products/index.js +2 -1
  11. package/dist/_internal/server/features/products/list-public.d.ts +130 -0
  12. package/dist/_internal/server/features/products/list-public.js +375 -0
  13. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  14. package/dist/_internal/server/features/reviews/data.js +3 -2
  15. package/dist/_internal/server/features/tester/visibility.js +3 -3
  16. package/dist/_internal/server/jobs/core/auctionSettlement.js +89 -16
  17. package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
  18. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
  19. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
  20. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
  21. package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
  22. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -0
  23. package/dist/_internal/server/jobs/handlers/messages.js +5 -0
  24. package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
  25. package/dist/_internal/shared/checkout/lanes.js +106 -0
  26. package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
  27. package/dist/_internal/shared/checkout/order-math.js +30 -6
  28. package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
  29. package/dist/_internal/shared/features/promotions/schema.js +0 -2
  30. package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
  31. package/dist/_internal/shared/features/reviews/config.js +8 -1
  32. package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
  33. package/dist/_internal/shared/listing-types/_registry.js +63 -1
  34. package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
  35. package/dist/_internal/shared/listing-types/art/config.js +12 -0
  36. package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
  37. package/dist/_internal/shared/listing-types/auction/config.js +17 -0
  38. package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
  39. package/dist/_internal/shared/listing-types/classified/config.js +14 -0
  40. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
  41. package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
  42. package/dist/_internal/shared/listing-types/feature-flags.d.ts +1 -0
  43. package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
  44. package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
  45. package/dist/_internal/shared/listing-types/live/config.js +14 -0
  46. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
  47. package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
  48. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
  49. package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
  50. package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
  51. package/dist/_internal/shared/listing-types/standard/config.js +9 -0
  52. package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
  53. package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
  54. package/dist/client.d.ts +7 -2
  55. package/dist/client.js +8 -2
  56. package/dist/constants/field-names.d.ts +12 -0
  57. package/dist/constants/field-names.js +12 -0
  58. package/dist/constants/table-keys.d.ts +2 -0
  59. package/dist/constants/table-keys.js +2 -0
  60. package/dist/features/account/components/NotificationBell.js +1 -0
  61. package/dist/features/account/components/UserOffersPanel.js +8 -1
  62. package/dist/features/account/components/UserOrdersView.js +27 -5
  63. package/dist/features/admin/actions/notification-actions.js +1 -1
  64. package/dist/features/admin/components/AdminAddressesView.js +3 -1
  65. package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
  66. package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
  67. package/dist/features/admin/components/AdminProductsView.js +34 -18
  68. package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
  69. package/dist/features/admin/constants/filter-tabs.js +22 -9
  70. package/dist/features/admin/schemas/firestore.d.ts +2 -1
  71. package/dist/features/admin/schemas/firestore.js +1 -0
  72. package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
  73. package/dist/features/auctions/actions/bid-actions.js +29 -15
  74. package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
  75. package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
  76. package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
  77. package/dist/features/auctions/components/AuctionsListView.js +12 -53
  78. package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
  79. package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
  80. package/dist/features/auctions/repository/bid.repository.js +19 -0
  81. package/dist/features/auctions/schemas/firestore.d.ts +11 -1
  82. package/dist/features/auctions/schemas/firestore.js +1 -0
  83. package/dist/features/cart/actions/cart-actions.d.ts +11 -0
  84. package/dist/features/cart/actions/cart-actions.js +24 -0
  85. package/dist/features/cart/repository/cart.repository.d.ts +24 -1
  86. package/dist/features/cart/repository/cart.repository.js +71 -6
  87. package/dist/features/cart/schemas/firestore.d.ts +23 -4
  88. package/dist/features/categories/components/BrandDetailTabs.js +31 -11
  89. package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
  90. package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
  91. package/dist/features/categories/components/CategoryProductsListing.js +5 -2
  92. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
  93. package/dist/features/contact/email.js +56 -1
  94. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
  95. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  96. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  97. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  98. package/dist/features/layout/AppLayoutShell.js +9 -2
  99. package/dist/features/layout/BottomActions.js +26 -3
  100. package/dist/features/layout/BottomActionsContext.d.ts +14 -0
  101. package/dist/features/layout/BottomActionsContext.js +13 -1
  102. package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
  103. package/dist/features/layout/hooks/useBottomActions.js +4 -1
  104. package/dist/features/layout/index.d.ts +1 -1
  105. package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
  106. package/dist/features/orders/repository/orders.repository.d.ts +4 -15
  107. package/dist/features/orders/repository/orders.repository.js +4 -27
  108. package/dist/features/orders/types/index.d.ts +16 -0
  109. package/dist/features/orders/utils/order-splitter.js +14 -2
  110. package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
  111. package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
  112. package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
  113. package/dist/features/products/components/ArtStickersListView.js +10 -49
  114. package/dist/features/products/components/AuctionsIndexListing.js +1 -2
  115. package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
  116. package/dist/features/products/components/ListingBottomActions.js +25 -0
  117. package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
  118. package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
  119. package/dist/features/products/components/ProductDetailActions.js +4 -0
  120. package/dist/features/products/components/ProductDetailPageView.js +7 -7
  121. package/dist/features/products/components/ProductsIndexPageView.js +9 -62
  122. package/dist/features/products/constants/listing-tabs.d.ts +43 -198
  123. package/dist/features/products/constants/listing-tabs.js +71 -58
  124. package/dist/features/products/constants/sieve.d.ts +104 -1
  125. package/dist/features/products/constants/sieve.js +27 -7
  126. package/dist/features/products/repository/products.repository.d.ts +45 -1
  127. package/dist/features/products/repository/products.repository.js +67 -8
  128. package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
  129. package/dist/features/products/utils/listing-badge-variant.js +26 -1
  130. package/dist/features/products/utils/listing-type.d.ts +24 -0
  131. package/dist/features/products/utils/listing-type.js +48 -0
  132. package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
  133. package/dist/features/promotions/actions/coupon-actions.js +12 -1
  134. package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
  135. package/dist/features/promotions/actions/coupon-stacking.js +21 -0
  136. package/dist/features/promotions/actions/index.d.ts +1 -0
  137. package/dist/features/promotions/actions/index.js +1 -0
  138. package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
  139. package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
  140. package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
  141. package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
  142. package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
  143. package/dist/features/promotions/components/index.d.ts +2 -0
  144. package/dist/features/promotions/components/index.js +1 -0
  145. package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
  146. package/dist/features/promotions/constants/coupon-help.js +42 -0
  147. package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
  148. package/dist/features/promotions/repository/coupons.repository.js +22 -8
  149. package/dist/features/promotions/schemas/firestore.d.ts +0 -1
  150. package/dist/features/promotions/schemas/firestore.js +1 -1
  151. package/dist/features/promotions/schemas/index.d.ts +0 -20
  152. package/dist/features/promotions/schemas/index.js +0 -1
  153. package/dist/features/reviews/actions/review-actions.js +5 -1
  154. package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
  155. package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
  156. package/dist/features/reviews/components/ReviewFilters.js +4 -3
  157. package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
  158. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
  159. package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
  160. package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
  161. package/dist/features/reviews/components/index.d.ts +2 -0
  162. package/dist/features/reviews/components/index.js +1 -0
  163. package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
  164. package/dist/features/reviews/hooks/useReviews.js +14 -6
  165. package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
  166. package/dist/features/reviews/repository/reviews.repository.js +77 -41
  167. package/dist/features/reviews/types/index.d.ts +8 -1
  168. package/dist/features/seller/actions/offer-actions.d.ts +6 -0
  169. package/dist/features/seller/actions/offer-actions.js +8 -9
  170. package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
  171. package/dist/features/seller/components/SellerOffersView.js +59 -8
  172. package/dist/features/seller/components/SellerProductsCards.js +2 -2
  173. package/dist/features/seller/components/SellerProductsView.js +21 -18
  174. package/dist/features/seller/repository/offer.repository.d.ts +18 -2
  175. package/dist/features/seller/repository/offer.repository.js +38 -2
  176. package/dist/features/seller/schemas/firestore.d.ts +7 -2
  177. package/dist/features/seller/schemas/firestore.js +3 -0
  178. package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
  179. package/dist/features/seller/schemas/offer-forms.js +28 -0
  180. package/dist/features/stores/actions/store-query-actions.js +1 -1
  181. package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
  182. package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
  183. package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
  184. package/dist/features/stores/components/StoreReviewsListing.js +7 -108
  185. package/dist/features/stores/schemas/index.d.ts +2 -2
  186. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
  187. package/dist/features/tester/seed-data/index.d.ts +1 -0
  188. package/dist/features/tester/seed-data/index.js +1 -0
  189. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
  190. package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
  191. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
  192. package/dist/index.d.ts +11 -4
  193. package/dist/index.js +15 -3
  194. package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
  195. package/dist/react/hooks/useListingTypeFlags.js +4 -9
  196. package/dist/schemas/registry.d.ts +0 -12
  197. package/dist/seed/claimed-coupons-seed-data.js +3 -3
  198. package/dist/seed/coupons-seed-data.js +29 -10
  199. package/dist/seed/faq-seed-data.js +2 -2
  200. package/dist/seed/manifest.js +2 -2
  201. package/dist/server-entry.d.ts +1 -0
  202. package/dist/server-entry.js +1 -0
  203. package/dist/server.d.ts +1 -0
  204. package/dist/server.js +3 -0
  205. package/dist/styles.css +44 -39
  206. package/dist/tailwind-utilities.css +1 -1
  207. package/dist/ui/components/FilterChipGroup.d.ts +21 -3
  208. package/dist/ui/components/FilterChipGroup.js +33 -4
  209. package/dist/ui/components/HorizontalScroller.d.ts +1 -2
  210. package/dist/ui/components/HorizontalScroller.js +9 -5
  211. package/dist/ui/components/HorizontalScroller.style.css +43 -38
  212. package/package.json +1 -1
@@ -9,8 +9,6 @@ const __P = {
9
9
  };
10
10
  const CLS_BREADCRUMB_LINK = "hover:text-primary-600 transition-colors";
11
11
  const CLS_LIVE_BADGE = "inline-block rounded-full bg-warning-surface dark:bg-warning-surface px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-warning dark:text-warning";
12
- const CLS_STAR_ON = "text-warning";
13
- const CLS_STAR_OFF = "text-zinc-200 dark:text-zinc-700";
14
12
  import { ROUTES } from "../../../next";
15
13
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
16
14
  import { formatCurrency } from "../../../utils/number.formatter";
@@ -30,7 +28,7 @@ import { FeatureBadgeList } from "../../products/components/FeatureBadge";
30
28
  import { HistoryTracker } from "../../history/components/HistoryTracker";
31
29
  import { ShareButton } from "../../products/components/ShareButton";
32
30
  import { MarketplaceAuctionGrid } from "./MarketplaceAuctionGrid";
33
- import { listReviewsBySeller } from "../../reviews/actions/review-actions";
31
+ import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
34
32
  import { PlaceBidModalButton } from "./PlaceBidFormClient";
35
33
  import { CollapsibleBidHistory } from "./CollapsibleBidHistory";
36
34
  import { LiveBidPrice } from "./LiveBidPrice";
@@ -50,11 +48,18 @@ function renderAuctionInfoPanel(props) {
50
48
  const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
51
49
  return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowPrice !== null && !isEnded && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && _jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" })] }) }))] }));
52
50
  }
53
- function renderAuctionStoreReviews(storeReviews) {
54
- if (storeReviews.length === 0)
51
+ /**
52
+ * Store reviews for the auction's seller.
53
+ *
54
+ * Was a bespoke `slice(0, 10)` block over `listReviewsBySeller()` — which concatenates
55
+ * per-product batches, so the "latest 10" it showed were in product order, not date
56
+ * order, and reviews 11+ were unreachable. `ReviewsListingPanel` pages the real
57
+ * store-reviews endpoint instead.
58
+ */
59
+ function renderAuctionStoreReviews(storeSlug) {
60
+ if (!storeSlug)
55
61
  return null;
56
- const avg = storeReviews.reduce((s, r) => s + r.rating, 0) / storeReviews.length;
57
- return (_jsxs(Section, { className: "mt-10", children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "xl", weight: "semibold", children: "Store Reviews" }), _jsxs(Row, { className: "mb-4", align: "center", gap: "3", children: [_jsx(Span, { weight: "bold", size: "3xl", color: "primary", children: avg.toFixed(1) }), _jsxs(Div, { children: [_jsx(Row, { gap: "xs", children: [1, 2, 3, 4, 5].map((star) => _jsx(Span, { className: star <= Math.round(avg) ? CLS_STAR_ON : CLS_STAR_OFF, children: "\u2605" }, star)) }), _jsxs(Text, { size: "xs", color: "muted", children: [storeReviews.length, " review", storeReviews.length !== 1 ? "s" : ""] })] })] }), _jsx(Stack, { gap: "sm", children: storeReviews.slice(0, 10).map((review) => (_jsxs(Stack, { gap: "xs", surface: "card", padding: "sm", className: "", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Span, { size: "sm", weight: "medium", color: "primary", children: review.userName }), _jsx(Row, { gap: "xs", children: [1, 2, 3, 4, 5].map((star) => _jsx(Span, { size: "xs", className: star <= review.rating ? CLS_STAR_ON : CLS_STAR_OFF, children: "\u2605" }, star)) })] }) }), review.title && _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: review.title }), _jsx(Text, { className: "leading-relaxed", color: "muted", size: "sm", children: review.comment }), _jsx(Text, { size: "xs", color: "faint", children: review.productTitle })] }, review.id))) })] }));
62
+ return (_jsxs(Section, { className: "mt-10", children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "xl", weight: "semibold", children: "Store Reviews" }), _jsx(ReviewsListingPanel, { source: { kind: "store", storeSlug }, stateMode: "local", context: "store", emptyLabel: "This store has no reviews yet." })] }));
58
63
  }
59
64
  export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, onBuyNow, productFeatures }) {
60
65
  const product = initialAuction !== undefined
@@ -64,9 +69,6 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
64
69
  ? await listBidsByProduct(String(product.id), { pageSize: 5 }).catch(() => null)
65
70
  : null;
66
71
  const storeId = product?.storeId;
67
- const storeReviews = storeId
68
- ? await listReviewsBySeller(storeId).catch(() => [])
69
- : [];
70
72
  if (!product) {
71
73
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Auction Not Found" }), _jsx(Text, { color: "muted", children: "This auction may have ended or the link is incorrect." }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Auctions" })] }) }) }) }));
72
74
  }
@@ -191,5 +193,5 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
191
193
  slug: typeof r.slug === "string" ? r.slug : undefined,
192
194
  }));
193
195
  return (_jsxs(Stack, { gap: "xl", children: [related.length > 0 && (_jsx(RelatedProducts, { labels: { title: "Similar Auctions" }, renderGrid: () => (_jsx(MarketplaceAuctionGrid, { auctions: related, gridClassName: "grid grid-cols-2 gap-[var(--appkit-space-4)] sm:grid-cols-3 lg:grid-cols-4" })) })), _jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] }));
194
- } }), renderAuctionStoreReviews(storeReviews), _jsx(AuctionBottomActions, { currentBid: currentBid, currency: currency, bidCount: bidCount, isEnded: isEnded, auctionEndDate: endDate })] })] }));
196
+ } }), renderAuctionStoreReviews(storeId), _jsx(AuctionBottomActions, { currentBid: currentBid, currency: currency, bidCount: bidCount, isEnded: isEnded, auctionEndDate: endDate })] })] }));
195
197
  }
@@ -1,62 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
3
- import { productRepository } from "../../../repositories";
4
2
  import { Container, Heading, Main, Section } from "../../../ui";
5
3
  import { AdSlot } from "../../homepage/components/AdSlot";
6
- import { parseListingSearchParams } from "../../../utils/listing-params";
7
4
  import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing";
8
- const DEFAULT_PAGE = 1;
5
+ import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
6
+ const AUCTION_LISTING_TYPES = ["auction"];
9
7
  const DEFAULT_PAGE_SIZE = 24;
10
8
  const DEFAULT_SORT = "auctionEndDate";
11
- function sp(params, key) {
12
- const v = params[key];
13
- return Array.isArray(v) ? v[0] ?? "" : v ?? "";
14
- }
15
- function buildAuctionFilters(params) {
16
- const parts = ["status==published", "listingType==auction"];
17
- const minBid = sp(params, "minBid");
18
- const maxBid = sp(params, "maxBid");
19
- if (minBid)
20
- parts.push(sieveFilter("currentBid", SIEVE_OP.GTE, minBid));
21
- if (maxBid)
22
- parts.push(sieveFilter("currentBid", SIEVE_OP.LTE, maxBid));
23
- const store = sp(params, "store");
24
- if (store) {
25
- const values = store.split("|").filter(Boolean);
26
- if (values.length === 1)
27
- parts.push(sieveFilter("storeId", SIEVE_OP.EQ, values[0]));
28
- else if (values.length > 1)
29
- parts.push(sieveFilter("storeId", SIEVE_OP.EQ, values.join("|")));
30
- }
31
- // Mirror AuctionsIndexListing's client-side default (Root Cause pattern —
32
- // SSR initialData is seeded into React Query with staleTime:Infinity, so if
33
- // this SSR filter doesn't already exclude ended auctions, the client never
34
- // refetches and ended auctions leak into the default "Show ended" off view).
35
- const showEnded = sp(params, "showEnded") === "true";
36
- const dateFrom = showEnded
37
- ? sp(params, "dateFrom")
38
- : sp(params, "dateFrom") || new Date().toISOString();
39
- const dateTo = sp(params, "dateTo");
40
- if (dateFrom)
41
- parts.push(sieveFilter("auctionEndDate", SIEVE_OP.GTE, dateFrom));
42
- if (dateTo)
43
- parts.push(sieveFilter("auctionEndDate", SIEVE_OP.LTE, dateTo));
44
- return parts.join(",");
45
- }
46
9
  export async function AuctionsListView({ searchParams = {} }) {
47
- const std = parseListingSearchParams(searchParams);
48
- const sort = std.sorts ?? DEFAULT_SORT;
49
- const page = std.page ?? DEFAULT_PAGE;
50
- const pageSize = std.pageSize ?? DEFAULT_PAGE_SIZE;
51
- const filters = buildAuctionFilters(searchParams);
52
- const result = await productRepository
53
- .list({
54
- filters,
55
- sorts: sort,
56
- page,
57
- pageSize,
58
- })
59
- .catch(() => null);
60
- const initial = result ?? null;
10
+ // Shared with /api/products. `hideEndedByDefault` mirrors
11
+ // AuctionsIndexListing's `showEnded ? … : dateFrom=now` default; because the
12
+ // default sort IS auctionEndDate, listPublicProducts pushes that range into
13
+ // Firestore rather than filtering in memory.
14
+ const initial = await listPublicProducts(parsePublicProductParams(searchParams, {
15
+ listingTypes: AUCTION_LISTING_TYPES,
16
+ pageSize: DEFAULT_PAGE_SIZE,
17
+ sorts: DEFAULT_SORT,
18
+ hideEndedByDefault: true,
19
+ }));
61
20
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Live Auctions" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(AuctionsIndexListing, { initialData: initial }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
62
21
  }
@@ -96,7 +96,16 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
96
96
  return;
97
97
  }
98
98
  setSuccess(true);
99
- router.refresh();
99
+ // Buy-Now writes a locked cart line, so send the buyer straight to the
100
+ // auction checkout lane to pay for it. This used to just call
101
+ // `router.refresh()` and discard the action's result entirely, leaving
102
+ // the buyer on the product page with no indication that anything was
103
+ // owed — the action returned an orderId nobody ever used.
104
+ const checkoutUrl = result?.checkoutUrl;
105
+ if (checkoutUrl)
106
+ router.push(checkoutUrl);
107
+ else
108
+ router.refresh();
100
109
  }
101
110
  catch (err) {
102
111
  void normalizeError(err);
@@ -50,6 +50,21 @@ export declare class BidRepository extends BaseRepository<BidDocument> {
50
50
  data: BidDocument;
51
51
  } | null>;
52
52
  markWon(batch: WriteBatch, ref: DocumentReference): void;
53
+ /**
54
+ * Link a won bid to the order it eventually became.
55
+ *
56
+ * `BidDocument` had no `orderId` at all before 2026-08-21, so a won bid and
57
+ * the thing the buyer paid for were two unrelated records — the "Won" row in
58
+ * /user/bids could not link anywhere, and nothing could tell a settled win
59
+ * from an unpaid one.
60
+ */
61
+ attachOrder(bidId: string, orderId: string): Promise<void>;
62
+ /**
63
+ * The winner never paid within their checkout window. Distinct from "lost"
64
+ * (outbid) — this bidder won and then defaulted, which a seller may want to
65
+ * act on.
66
+ */
67
+ markForfeited(bidId: string): Promise<void>;
53
68
  markLost(batch: WriteBatch, ref: DocumentReference): void;
54
69
  markOutbid(batch: WriteBatch, ref: DocumentReference): void;
55
70
  markWinning(batch: WriteBatch, ref: DocumentReference): void;
@@ -122,6 +122,25 @@ export class BidRepository extends BaseRepository {
122
122
  updatedAt: new Date(),
123
123
  });
124
124
  }
125
+ /**
126
+ * Link a won bid to the order it eventually became.
127
+ *
128
+ * `BidDocument` had no `orderId` at all before 2026-08-21, so a won bid and
129
+ * the thing the buyer paid for were two unrelated records — the "Won" row in
130
+ * /user/bids could not link anywhere, and nothing could tell a settled win
131
+ * from an unpaid one.
132
+ */
133
+ async attachOrder(bidId, orderId) {
134
+ await this.update(bidId, { orderId, updatedAt: new Date() });
135
+ }
136
+ /**
137
+ * The winner never paid within their checkout window. Distinct from "lost"
138
+ * (outbid) — this bidder won and then defaulted, which a seller may want to
139
+ * act on.
140
+ */
141
+ async markForfeited(bidId) {
142
+ await this.update(bidId, { status: "forfeited", isWinning: false, updatedAt: new Date() });
143
+ }
125
144
  markLost(batch, ref) {
126
145
  batch.update(ref, {
127
146
  status: "lost",
@@ -3,7 +3,9 @@
3
3
  */
4
4
  import { type GenerateBidIdInput } from "../../../utils/id-generators";
5
5
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
6
- export type BidStatus = "active" | "outbid" | "won" | "lost" | "cancelled";
6
+ export type BidStatus = "active" | "outbid" | "won" | "lost" | "cancelled"
7
+ /** Won, then the buyer let the 48h payment window lapse. */
8
+ | "forfeited";
7
9
  /** Runtime-accessible bid status values — use instead of bare string literals. */
8
10
  export declare const BidStatusValues: {
9
11
  readonly ACTIVE: "active";
@@ -11,6 +13,7 @@ export declare const BidStatusValues: {
11
13
  readonly WON: "won";
12
14
  readonly LOST: "lost";
13
15
  readonly CANCELLED: "cancelled";
16
+ readonly FORFEITED: "forfeited";
14
17
  };
15
18
  export interface BidDocument extends BaseDocument {
16
19
  productId: string;
@@ -25,6 +28,13 @@ export interface BidDocument extends BaseDocument {
25
28
  previousBidAmount?: number;
26
29
  bidDate: Date;
27
30
  autoMaxBid?: number;
31
+ /**
32
+ * The order this winning bid was settled into. Written by
33
+ * `finalizeLockedLines` once the winner actually pays; absent on a won-but-
34
+ * unpaid bid, which is what lets /user/bids show a "Pay now" CTA rather than
35
+ * a dead status badge.
36
+ */
37
+ orderId?: string;
28
38
  }
29
39
  export declare const BID_COLLECTION: "bids";
30
40
  export declare const BID_INDEXED_FIELDS: readonly ["productId", "userId", "status", "isWinning", "bidDate", "createdAt"];
@@ -9,6 +9,7 @@ export const BidStatusValues = {
9
9
  WON: "won",
10
10
  LOST: "lost",
11
11
  CANCELLED: "cancelled",
12
+ FORFEITED: "forfeited",
12
13
  };
13
14
  export const BID_COLLECTION = "bids";
14
15
  export const BID_INDEXED_FIELDS = [
@@ -6,6 +6,17 @@
6
6
  */
7
7
  import type { AddToCartInput, CartDocument, UpdateCartItemInput } from "../schemas";
8
8
  export declare function addItemToCart(userId: string, input: AddToCartInput): Promise<CartDocument>;
9
+ /** Error code the client maps to a lane-aware message. */
10
+ export declare const CART_LANE_BLOCKED = "CART_LANE_BLOCKED";
11
+ /**
12
+ * Won-auction lines are non-removable and fixed-quantity — the buyer committed
13
+ * to the purchase by bidding, and letting them delete it would turn every
14
+ * auction into a free option. That's already enforced one layer down:
15
+ * `cartRepository.updateItem` / `removeItem` both reject a `locked` line, and
16
+ * auction settlement is the only writer that sets `locked: true`. Accepted
17
+ * offers deliberately do NOT set it — declining to buy is the buyer's right,
18
+ * and the offer simply lapses at its `checkoutDeadline`.
19
+ */
9
20
  export declare function updateCartItem(userId: string, itemId: string, input: UpdateCartItemInput): Promise<CartDocument>;
10
21
  export declare function removeCartItem(userId: string, itemId: string): Promise<CartDocument>;
11
22
  export declare function clearCart(userId: string): Promise<CartDocument>;
@@ -11,6 +11,7 @@ import { productRepository } from "../../products/repository/products.repository
11
11
  import { storeRepository } from "../../stores/repository/store.repository";
12
12
  import { normalizeListingType } from "../../products/utils/listing-type";
13
13
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
14
+ import { CART_LANE, activeLane, canAddNewItems, } from "../../../_internal/shared/checkout/lanes";
14
15
  /**
15
16
  * Product documents don't always carry a denormalized `storeName` — the
16
17
  * create routes only set it when the caller explicitly passes one, so
@@ -35,9 +36,32 @@ export async function addItemToCart(userId, input) {
35
36
  if (!canAddToCart(input.listingType)) {
36
37
  throw new ValidationError(`Listings of type "${input.listingType}" cannot be added to the cart.`);
37
38
  }
39
+ // Lane gate. While a higher-obligation lane (a won auction, then an accepted
40
+ // offer) is outstanding, no NEW shopping may be added — otherwise the buyer
41
+ // can keep deferring something they already committed to by piling more into
42
+ // the cart. Enforced here, on the server, not only in the UI: the UI hint is
43
+ // a courtesy, this is the rule.
44
+ const cart = await cartRepository.getOrCreate(userId);
45
+ if (!canAddNewItems(cart.items ?? [])) {
46
+ const blocking = activeLane(cart.items ?? []);
47
+ throw new ValidationError(blocking === CART_LANE.AUCTION
48
+ ? "Settle your won auction first — you can add other items once it's paid for."
49
+ : "Complete your accepted offer first — you can add other items once it's paid for.", { code: CART_LANE_BLOCKED });
50
+ }
38
51
  const storeName = await resolveStoreName(input.storeId, input.storeName);
39
52
  return cartRepository.addItem(userId, { ...input, storeName });
40
53
  }
54
+ /** Error code the client maps to a lane-aware message. */
55
+ export const CART_LANE_BLOCKED = "CART_LANE_BLOCKED";
56
+ /**
57
+ * Won-auction lines are non-removable and fixed-quantity — the buyer committed
58
+ * to the purchase by bidding, and letting them delete it would turn every
59
+ * auction into a free option. That's already enforced one layer down:
60
+ * `cartRepository.updateItem` / `removeItem` both reject a `locked` line, and
61
+ * auction settlement is the only writer that sets `locked: true`. Accepted
62
+ * offers deliberately do NOT set it — declining to buy is the buyer's right,
63
+ * and the offer simply lapses at its `checkoutDeadline`.
64
+ */
41
65
  export async function updateCartItem(userId, itemId, input) {
42
66
  return cartRepository.updateItem(userId, itemId, input);
43
67
  }
@@ -1,7 +1,7 @@
1
1
  import type { DocumentReference } from "firebase-admin/firestore";
2
2
  import type { DocumentSnapshot } from "../../../providers/db-firebase";
3
3
  import { BaseRepository, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
4
- import { type AddToCartInput, type CartAppliedCoupon, type CartDocument, type UpdateCartItemInput } from "../schemas";
4
+ import { type AddToCartInput, type CartAppliedCoupon, type CartDocument, type CartItemDocument, type UpdateCartItemInput } from "../schemas";
5
5
  export declare class CartRepository extends BaseRepository<CartDocument> {
6
6
  private static readonly SIEVE_FIELDS;
7
7
  constructor();
@@ -19,6 +19,29 @@ export declare class CartRepository extends BaseRepository<CartDocument> {
19
19
  getSubtotal(cart: CartDocument): number;
20
20
  addCoupon(userId: string, coupon: CartAppliedCoupon): Promise<void>;
21
21
  removeCoupon(userId: string, code: string): Promise<void>;
22
+ /**
23
+ * Drop every locked line matching a predicate, bypassing the `locked` guard
24
+ * that blocks user-initiated removal.
25
+ *
26
+ * Only the expiry sweeps call this: a locked line whose claim has lapsed must
27
+ * be cleared, or it keeps the buyer's auction/offer lane non-empty forever
28
+ * and the lane gate blocks their ordinary checkout on something they can no
29
+ * longer act on.
30
+ */
31
+ private removeLockedLines;
32
+ /** Clear the cart line created for a specific accepted offer. */
33
+ removeItemsByOfferId(userId: string, offerId: string): Promise<number>;
34
+ /** Clear the cart line created for a specific won auction bid. */
35
+ removeItemsByBidId(userId: string, bidId: string): Promise<number>;
36
+ /**
37
+ * Every locked line across all carts whose `checkoutDeadline` has passed.
38
+ * Bounded per Rule #6 — the sweep runs on a schedule, so a backlog drains
39
+ * over successive runs rather than in one unbounded scan.
40
+ */
41
+ findExpiredLockedLines(now: Date, limit?: number): Promise<Array<{
42
+ userId: string;
43
+ item: CartItemDocument;
44
+ }>>;
22
45
  updateItemShipping(userId: string, itemId: string, providerId: string, fee: number): Promise<CartDocument>;
23
46
  clearAllCoupons(userId: string): Promise<void>;
24
47
  setSelectedItems(userId: string, itemIds: string[] | null): Promise<void>;
@@ -61,14 +61,20 @@ export class CartRepository extends BaseRepository {
61
61
  try {
62
62
  const cart = await this.getOrCreate(userId);
63
63
  const items = [...cart.items];
64
- if (input.offerId) {
65
- const alreadyAdded = items.some((item) => item.offerId === input.offerId);
66
- if (alreadyAdded)
67
- return cart;
64
+ // A locked line (accepted offer / won auction) is identified by its
65
+ // offerId or bidId, never merged by productId — two separate wins on the
66
+ // same listing are two separate obligations, and re-running the
67
+ // settlement sweep must not duplicate a line it already wrote.
68
+ if (input.offerId && items.some((item) => item.offerId === input.offerId)) {
69
+ return cart;
68
70
  }
69
- const existingIndex = input.offerId
71
+ if (input.bidId && items.some((item) => item.bidId === input.bidId)) {
72
+ return cart;
73
+ }
74
+ const isLockedLine = Boolean(input.offerId || input.bidId);
75
+ const existingIndex = isLockedLine
70
76
  ? -1
71
- : items.findIndex((item) => item.productId === input.productId && !item.offerId);
77
+ : items.findIndex((item) => item.productId === input.productId && !item.offerId && !item.bidId);
72
78
  if (existingIndex >= 0) {
73
79
  items[existingIndex] = {
74
80
  ...items[existingIndex],
@@ -90,9 +96,15 @@ export class CartRepository extends BaseRepository {
90
96
  listingType: input.listingType,
91
97
  ...(input.isOffer !== undefined && { isOffer: input.isOffer }),
92
98
  ...(input.offerId !== undefined && { offerId: input.offerId }),
99
+ ...(input.isAuctionWin !== undefined && { isAuctionWin: input.isAuctionWin }),
100
+ ...(input.auctionId !== undefined && { auctionId: input.auctionId }),
101
+ ...(input.bidId !== undefined && { bidId: input.bidId }),
93
102
  ...(input.lockedPrice !== undefined && {
94
103
  lockedPrice: input.lockedPrice,
95
104
  }),
105
+ ...(input.checkoutDeadline !== undefined && {
106
+ checkoutDeadline: input.checkoutDeadline,
107
+ }),
96
108
  ...(input.locked !== undefined && { locked: input.locked }),
97
109
  // SB-UNI-4 2026-05-13 — propagate bundle identifiers when present.
98
110
  ...(input.bundleCategorySlug !== undefined && {
@@ -231,6 +243,59 @@ export class CartRepository extends BaseRepository {
231
243
  .doc(userId)
232
244
  .set({ appliedCoupons: updated, updatedAt: new Date() }, { merge: true });
233
245
  }
246
+ /**
247
+ * Drop every locked line matching a predicate, bypassing the `locked` guard
248
+ * that blocks user-initiated removal.
249
+ *
250
+ * Only the expiry sweeps call this: a locked line whose claim has lapsed must
251
+ * be cleared, or it keeps the buyer's auction/offer lane non-empty forever
252
+ * and the lane gate blocks their ordinary checkout on something they can no
253
+ * longer act on.
254
+ */
255
+ async removeLockedLines(userId, matches) {
256
+ const cart = await this.findByUserId(userId);
257
+ if (!cart)
258
+ return 0;
259
+ const items = cart.items.filter((item) => !matches(item));
260
+ const removed = cart.items.length - items.length;
261
+ if (removed === 0)
262
+ return 0;
263
+ await this.db
264
+ .collection(this.collection)
265
+ .doc(userId)
266
+ .set(prepareForFirestore({ ...cart, items, updatedAt: new Date() }));
267
+ return removed;
268
+ }
269
+ /** Clear the cart line created for a specific accepted offer. */
270
+ async removeItemsByOfferId(userId, offerId) {
271
+ return this.removeLockedLines(userId, (item) => item.offerId === offerId);
272
+ }
273
+ /** Clear the cart line created for a specific won auction bid. */
274
+ async removeItemsByBidId(userId, bidId) {
275
+ return this.removeLockedLines(userId, (item) => item.bidId === bidId);
276
+ }
277
+ /**
278
+ * Every locked line across all carts whose `checkoutDeadline` has passed.
279
+ * Bounded per Rule #6 — the sweep runs on a schedule, so a backlog drains
280
+ * over successive runs rather than in one unbounded scan.
281
+ */
282
+ async findExpiredLockedLines(now, limit = 200) {
283
+ const snapshot = await this.getCollection().limit(limit).get();
284
+ const out = [];
285
+ for (const doc of snapshot.docs) {
286
+ const cart = this.mapDoc(doc);
287
+ for (const item of cart.items ?? []) {
288
+ if (!item.checkoutDeadline)
289
+ continue;
290
+ const deadline = item.checkoutDeadline instanceof Date
291
+ ? item.checkoutDeadline
292
+ : new Date(item.checkoutDeadline);
293
+ if (deadline <= now)
294
+ out.push({ userId: cart.userId ?? doc.id, item });
295
+ }
296
+ }
297
+ return out;
298
+ }
234
299
  async updateItemShipping(userId, itemId, providerId, fee) {
235
300
  try {
236
301
  const cart = await this.findByUserId(userId);
@@ -20,15 +20,32 @@ export interface CartItemDocument {
20
20
  /**
21
21
  * Snapshot of the product's listing-kind at add-to-cart time (SB1-G Phase 4).
22
22
  * Drives order-grouping (auctions/pre-orders settle separately from standard
23
- * carts) and cart-side UI badges. Replaces the legacy `isAuction`/`isPreOrder`
23
+ * carts) and cart-side UI badges. Replaces the legacy `isAuctionWin`/`isPreOrder`
24
24
  * pair on the cart item.
25
25
  */
26
26
  listingType: ListingType;
27
27
  /** True when item was added from an accepted Make-an-Offer */
28
28
  isOffer?: boolean;
29
29
  offerId?: string;
30
- /** Locked offer price — overrides normal product price at checkout */
30
+ /**
31
+ * True when the line was created by auction settlement for the winning
32
+ * bidder. Auctions are `canAddToCart: false` for user-initiated adds — this
33
+ * line is written by the settlement job through the repository directly, and
34
+ * is the ONLY way an auction reaches the cart.
35
+ */
36
+ isAuctionWin?: boolean;
37
+ /** The auction product this win refers to (= productId, kept explicit). */
38
+ auctionId?: string;
39
+ /** The winning bid, so the resulting order can be linked back to it. */
40
+ bidId?: string;
41
+ /** Locked offer/winning-bid price — overrides normal product price at checkout */
31
42
  lockedPrice?: number;
43
+ /**
44
+ * When the claim on this locked price lapses. Mirrors the offer's own
45
+ * `checkoutDeadline`; set to now + 48h for a won auction. The expiry sweep
46
+ * clears lines past this point.
47
+ */
48
+ checkoutDeadline?: Date;
32
49
  /** When true the item cannot be removed or have its quantity changed. Set on won-auction and accepted-offer items that require mandatory payment. */
33
50
  locked?: boolean;
34
51
  /**
@@ -65,8 +82,6 @@ export interface CartAppliedCoupon {
65
82
  storeId?: string;
66
83
  /** Item IDs (CartItemDocument.itemId) this coupon was calculated against */
67
84
  applicableItemIds?: string[];
68
- /** Mirrors CouponDocument.restrictions.combineWithSellerCoupons — used for conflict detection when adding future coupons */
69
- combineWithSellerCoupons?: boolean;
70
85
  }
71
86
  export interface CartDocument extends BaseDocument {
72
87
  userId: string;
@@ -114,7 +129,11 @@ export type AddToCartInput = {
114
129
  listingType: ListingType;
115
130
  isOffer?: boolean;
116
131
  offerId?: string;
132
+ isAuctionWin?: boolean;
133
+ auctionId?: string;
134
+ bidId?: string;
117
135
  lockedPrice?: number;
136
+ checkoutDeadline?: Date;
118
137
  locked?: boolean;
119
138
  /** SB-UNI-4 2026-05-13 — bundle identifier when the line is a bundle. */
120
139
  bundleCategorySlug?: string;
@@ -7,14 +7,35 @@ import { AuctionsIndexListing } from "../../products/components/AuctionsIndexLis
7
7
  import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing";
8
8
  import { PrizeDrawsIndexListing } from "../../products/components/PrizeDrawsIndexListing";
9
9
  import { CategoryBundlesListing } from "./CategoryBundlesListing";
10
- import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
10
+ import { CATEGORY_PAGE_TABS, ART_STICKERS_LISTING_TYPES, } from "../../products/constants/listing-tabs";
11
+ import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
12
+ import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
13
+ /**
14
+ * Tab slug → listing/category type, derived (2026-08-21). The hand-written
15
+ * version covered 5 of 10 tabs, and this view's filter did `if (!mapping)
16
+ * return false` — so classifieds, digital codes, live items and art were
17
+ * silently DROPPED from every brand page rather than rendering blank (the
18
+ * failure mode its category-page twin had). Same root gap, opposite symptom.
19
+ *
20
+ * `stores` is intentionally absent: a brand has no store roster, which is the
21
+ * one real difference between this component and CategoryDetailTabs (see
22
+ * CLAUDE.md's Categories & Brands Reference on why the duplication stands).
23
+ */
11
24
  const TAB_TYPE_MAP = {
12
- products: { kind: "listing", type: "standard" },
13
- auctions: { kind: "listing", type: "auction" },
14
- "pre-orders": { kind: "listing", type: "pre-order" },
15
- "prize-draws": { kind: "listing", type: "prize-draw" },
25
+ ...Object.fromEntries(ALL_LISTING_TYPES.map((type) => [
26
+ pluginFor(type).tabSlug,
27
+ { kind: "listing", type },
28
+ ])),
16
29
  bundles: { kind: "category", type: "bundle" },
17
30
  };
31
+ /** Listing types rendered by the shared products listing rather than a dedicated one. */
32
+ const GENERIC_TAB_LISTING_TYPES = {
33
+ [pluginFor("standard").tabSlug]: ["standard"],
34
+ [pluginFor("classified").tabSlug]: ["classified"],
35
+ [pluginFor("digital-code").tabSlug]: ["digital-code"],
36
+ [pluginFor("live").tabSlug]: ["live"],
37
+ [pluginFor("art").tabSlug]: ART_STICKERS_LISTING_TYPES,
38
+ };
18
39
  export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
19
40
  const countFor = (id) => {
20
41
  switch (id) {
@@ -28,11 +49,10 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
28
49
  };
29
50
  const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
30
51
  const mapping = TAB_TYPE_MAP[t.id];
31
- // This view only has content renderers for the 5 tab ids in
32
- // TAB_TYPE_MAP (products/auctions/pre-orders/prize-draws/bundles) any
33
- // other CATEGORY_PAGE_TABS id (stores, classifieds, etc.) has no case in
34
- // the render switch below and would show a blank tab, so exclude those
35
- // here rather than rely on downstream JSX to silently render nothing.
52
+ // A tab with no mapping has no render branch below and would show a blank
53
+ // panel, so drop it here rather than let downstream JSX render nothing.
54
+ // Today that's only `stores` a brand has no store roster. Every listing
55
+ // type IS mapped now (it used to also silently drop four real types).
36
56
  if (!mapping)
37
57
  return false;
38
58
  if (mapping.kind === "listing" && enabledListingTypes) {
@@ -49,5 +69,5 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
49
69
  });
50
70
  const firstTabId = (visibleTabs[0]?.id ?? "products");
51
71
  const [activeTab, setActiveTab] = useState(firstTabId);
52
- return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), activeTab === "products" && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, initialData: initialProductsData })), activeTab === "auctions" && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === "pre-orders" && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === "prize-draws" && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
72
+ return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
53
73
  }