@mohasinac/appkit 4.13.0 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +14 -5
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +3 -0
  3. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +6 -0
  4. package/dist/_internal/client/features/lottery/LotteryDetailView.d.ts +8 -0
  5. package/dist/_internal/client/features/lottery/LotteryDetailView.js +4 -1
  6. package/dist/_internal/client/features/lottery/LotteryListView.d.ts +7 -0
  7. package/dist/_internal/client/features/lottery/LotteryListView.js +1 -1
  8. package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.js +4 -1
  9. package/dist/_internal/client/features/lottery/slot-collage-items.d.ts +15 -0
  10. package/dist/_internal/client/features/lottery/slot-collage-items.js +13 -0
  11. package/dist/_internal/server/features/auctions/actions.js +6 -0
  12. package/dist/_internal/server/features/bundles/data.d.ts +16 -0
  13. package/dist/_internal/server/features/bundles/data.js +26 -0
  14. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  15. package/dist/_internal/server/features/bundles/index.js +1 -1
  16. package/dist/_internal/server/features/checkout/actions.js +6 -0
  17. package/dist/_internal/server/features/checkout/locked-lines.d.ts +38 -0
  18. package/dist/_internal/server/features/checkout/locked-lines.js +171 -0
  19. package/dist/_internal/server/features/homepage/index.d.ts +0 -1
  20. package/dist/_internal/server/features/homepage/index.js +7 -1
  21. package/dist/_internal/server/features/lottery/adapters.js +4 -0
  22. package/dist/_internal/server/features/products/data.js +15 -37
  23. package/dist/_internal/server/features/products/index.d.ts +1 -1
  24. package/dist/_internal/server/features/products/index.js +1 -1
  25. package/dist/_internal/server/features/products/list-public.d.ts +46 -20
  26. package/dist/_internal/server/features/products/list-public.js +266 -90
  27. package/dist/_internal/server/features/products/listing-tab-counts.d.ts +44 -0
  28. package/dist/_internal/server/features/products/listing-tab-counts.js +97 -0
  29. package/dist/_internal/server/features/site-settings/adapters.d.ts +51 -0
  30. package/dist/_internal/server/features/site-settings/adapters.js +75 -0
  31. package/dist/_internal/server/features/site-settings/index.d.ts +1 -0
  32. package/dist/_internal/server/features/site-settings/index.js +1 -0
  33. package/dist/_internal/server/features/stores/adapters.d.ts +56 -0
  34. package/dist/_internal/server/features/stores/adapters.js +91 -0
  35. package/dist/_internal/server/features/stores/index.d.ts +1 -0
  36. package/dist/_internal/server/features/stores/index.js +1 -0
  37. package/dist/_internal/server/jobs/core/auctionSettlement.js +56 -2
  38. package/dist/_internal/server/jobs/core/offerExpiry.js +45 -9
  39. package/dist/_internal/server/jobs/core/paymentReviewAutoApprove.d.ts +5 -0
  40. package/dist/_internal/server/jobs/core/paymentReviewAutoApprove.js +33 -1
  41. package/dist/_internal/server/jobs/handlers/messages.d.ts +7 -0
  42. package/dist/_internal/server/jobs/handlers/messages.js +10 -0
  43. package/dist/_internal/shared/actions/action-registry.js +15 -1
  44. package/dist/_internal/shared/checkout/lanes.d.ts +25 -0
  45. package/dist/_internal/shared/checkout/lanes.js +26 -0
  46. package/dist/_internal/shared/features/auctions/config.d.ts +27 -0
  47. package/dist/_internal/shared/features/auctions/config.js +27 -0
  48. package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
  49. package/dist/_internal/shared/features/cart/schema.d.ts +12 -12
  50. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +32 -32
  51. package/dist/_internal/shared/features/emi/schedule.d.ts +24 -3
  52. package/dist/_internal/shared/features/emi/schedule.js +20 -5
  53. package/dist/_internal/shared/features/events/schema.d.ts +2 -2
  54. package/dist/_internal/shared/features/homepage/section-gate.d.ts +14 -0
  55. package/dist/_internal/shared/features/homepage/section-gate.js +51 -0
  56. package/dist/_internal/shared/features/offers/config.d.ts +30 -0
  57. package/dist/_internal/shared/features/offers/config.js +59 -0
  58. package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
  59. package/dist/_internal/shared/features/products/schema.d.ts +32 -32
  60. package/dist/_internal/shared/features/products/to-product-item.js +6 -0
  61. package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
  62. package/dist/_internal/shared/features/site-settings/fees.d.ts +60 -0
  63. package/dist/_internal/shared/features/site-settings/fees.js +39 -0
  64. package/dist/_internal/shared/features/site-settings/types.d.ts +40 -0
  65. package/dist/_internal/shared/features/site-settings/types.js +1 -0
  66. package/dist/_internal/shared/history/history.d.ts +66 -0
  67. package/dist/_internal/shared/history/history.js +158 -0
  68. package/dist/_internal/shared/history/index.d.ts +4 -0
  69. package/dist/_internal/shared/history/index.js +2 -0
  70. package/dist/_internal/shared/history/types.d.ts +61 -0
  71. package/dist/_internal/shared/history/types.js +6 -0
  72. package/dist/_internal/shared/listing-types/_registry.d.ts +54 -1
  73. package/dist/_internal/shared/listing-types/_registry.js +67 -0
  74. package/dist/_internal/shared/listing-types/art/config.d.ts +3 -0
  75. package/dist/_internal/shared/listing-types/art/config.js +5 -0
  76. package/dist/_internal/shared/listing-types/auction/config.d.ts +13 -0
  77. package/dist/_internal/shared/listing-types/auction/config.js +21 -0
  78. package/dist/_internal/shared/listing-types/availability.d.ts +79 -0
  79. package/dist/_internal/shared/listing-types/availability.js +104 -0
  80. package/dist/_internal/shared/listing-types/capabilities.d.ts +15 -0
  81. package/dist/_internal/shared/listing-types/capabilities.js +12 -0
  82. package/dist/_internal/shared/listing-types/classified/config.d.ts +3 -0
  83. package/dist/_internal/shared/listing-types/classified/config.js +5 -0
  84. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +11 -0
  85. package/dist/_internal/shared/listing-types/digital-code/config.js +22 -2
  86. package/dist/_internal/shared/listing-types/live/config.d.ts +3 -0
  87. package/dist/_internal/shared/listing-types/live/config.js +5 -0
  88. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +3 -0
  89. package/dist/_internal/shared/listing-types/pre-order/config.js +19 -0
  90. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +11 -0
  91. package/dist/_internal/shared/listing-types/prize-draw/config.js +15 -0
  92. package/dist/_internal/shared/listing-types/standard/config.d.ts +3 -0
  93. package/dist/_internal/shared/listing-types/standard/config.js +5 -0
  94. package/dist/_internal/shared/listing-types/stickers/config.d.ts +3 -0
  95. package/dist/_internal/shared/listing-types/stickers/config.js +5 -0
  96. package/dist/client.d.ts +7 -5
  97. package/dist/client.js +2 -2
  98. package/dist/constants/api-endpoints.d.ts +6 -0
  99. package/dist/constants/api-endpoints.js +2 -0
  100. package/dist/constants/field-names.d.ts +49 -2
  101. package/dist/constants/field-names.js +53 -2
  102. package/dist/constants/table-keys.d.ts +17 -0
  103. package/dist/constants/table-keys.js +17 -0
  104. package/dist/errors/error-codes.d.ts +13 -1
  105. package/dist/errors/error-codes.js +13 -1
  106. package/dist/errors/error-display-map.js +12 -3
  107. package/dist/errors/messages.d.ts +10 -2
  108. package/dist/errors/messages.js +10 -2
  109. package/dist/features/about/components/AboutView.d.ts +3 -0
  110. package/dist/features/about/components/AboutView.js +3 -2
  111. package/dist/features/about/components/PolicyPageView.d.ts +2 -1
  112. package/dist/features/about/components/PolicyPageView.js +61 -23
  113. package/dist/features/about/components/PublicProfileView.js +5 -0
  114. package/dist/features/about/schemas/firestore.d.ts +3 -0
  115. package/dist/features/account/components/UserOrdersView.js +7 -0
  116. package/dist/features/account/components/index.d.ts +0 -2
  117. package/dist/features/account/components/index.js +0 -1
  118. package/dist/features/account/schemas/index.d.ts +2 -2
  119. package/dist/features/account/types/index.d.ts +21 -2
  120. package/dist/features/addresses/schemas/index.d.ts +4 -4
  121. package/dist/features/admin/components/AdminArtView.js +8 -3
  122. package/dist/features/admin/components/AdminClassifiedView.js +8 -3
  123. package/dist/features/admin/components/AdminDigitalCodesView.js +8 -3
  124. package/dist/features/admin/components/AdminFeatureFlagsView.js +1 -0
  125. package/dist/features/admin/components/AdminLiveView.js +8 -3
  126. package/dist/features/admin/components/AdminOffersView.d.ts +4 -0
  127. package/dist/features/admin/components/AdminOffersView.js +186 -0
  128. package/dist/features/admin/components/AdminOrdersView.js +10 -1
  129. package/dist/features/admin/components/AdminPrizeDrawsView.js +5 -0
  130. package/dist/features/admin/components/AdminProductsView.js +16 -29
  131. package/dist/features/admin/components/AdminSiteSettingsView.js +31 -13
  132. package/dist/features/admin/components/AdminSiteView.js +6 -2
  133. package/dist/features/admin/components/AdminStickersView.js +8 -3
  134. package/dist/features/admin/components/index.d.ts +2 -0
  135. package/dist/features/admin/components/index.js +1 -0
  136. package/dist/features/admin/constants/filter-tabs.d.ts +86 -0
  137. package/dist/features/admin/constants/filter-tabs.js +55 -0
  138. package/dist/features/admin/schemas/firestore.d.ts +122 -3
  139. package/dist/features/admin/schemas/firestore.js +1 -0
  140. package/dist/features/auctions/actions/bid-actions.d.ts +20 -5
  141. package/dist/features/auctions/actions/bid-actions.js +93 -20
  142. package/dist/features/auctions/components/AuctionDetailPageView.js +11 -6
  143. package/dist/features/auctions/components/AuctionsListView.js +5 -6
  144. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +7 -0
  145. package/dist/features/auctions/components/MarketplaceAuctionCard.js +19 -2
  146. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -2
  147. package/dist/features/auctions/components/PlaceBidFormClient.js +8 -4
  148. package/dist/features/auctions/repository/bid.repository.d.ts +13 -0
  149. package/dist/features/auctions/repository/bid.repository.js +23 -0
  150. package/dist/features/auctions/schemas/firestore.d.ts +17 -0
  151. package/dist/features/auctions/schemas/index.d.ts +12 -12
  152. package/dist/features/auth/permissions/constants.d.ts +1 -1
  153. package/dist/features/auth/permissions/constants.js +10 -1
  154. package/dist/features/blog/actions/blog-actions.d.ts +7 -7
  155. package/dist/features/blog/schemas/index.d.ts +6 -6
  156. package/dist/features/cart/repository/cart.repository.js +1 -0
  157. package/dist/features/cart/schemas/firestore.d.ts +15 -0
  158. package/dist/features/catalogue/schemas/validation.d.ts +4 -4
  159. package/dist/features/categories/components/BrandDetailPageView.js +23 -40
  160. package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -7
  161. package/dist/features/categories/components/BrandDetailTabs.js +8 -12
  162. package/dist/features/categories/components/BundleCollage.js +1 -1
  163. package/dist/features/categories/components/CategoryDetailPageView.js +53 -57
  164. package/dist/features/categories/components/CategoryDetailTabs.d.ts +10 -8
  165. package/dist/features/categories/components/CategoryDetailTabs.js +11 -16
  166. package/dist/features/categories/components/CategoryProductsListing.js +11 -1
  167. package/dist/features/categories/components/ConcernCard.js +7 -1
  168. package/dist/features/categories/schemas/firestore.d.ts +13 -0
  169. package/dist/features/categories/schemas/index.d.ts +26 -26
  170. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +16 -1
  171. package/dist/features/classified/components/ClassifiedDetailPageView.js +23 -3
  172. package/dist/features/events/components/AdminEventsView.js +15 -11
  173. package/dist/features/events/components/EventFilters.d.ts +9 -1
  174. package/dist/features/events/components/EventFilters.js +45 -21
  175. package/dist/features/events/components/EventsIndexListing.d.ts +6 -1
  176. package/dist/features/events/components/EventsIndexListing.js +2 -2
  177. package/dist/features/events/components/EventsListPageView.js +13 -4
  178. package/dist/features/events/repository/events.repository.d.ts +17 -0
  179. package/dist/features/events/repository/events.repository.js +27 -0
  180. package/dist/features/events/schemas/firestore.d.ts +12 -0
  181. package/dist/features/events/schemas/firestore.js +12 -0
  182. package/dist/features/events/schemas/index.d.ts +50 -50
  183. package/dist/features/events/types/index.d.ts +11 -0
  184. package/dist/features/events/types/index.js +57 -1
  185. package/dist/features/faq/actions/faq-actions.d.ts +12 -12
  186. package/dist/features/faq/schemas/index.d.ts +8 -8
  187. package/dist/features/filters/FilterFacetSection.d.ts +7 -1
  188. package/dist/features/filters/FilterFacetSection.js +11 -3
  189. package/dist/features/grouped/schemas/index.d.ts +4 -4
  190. package/dist/features/homepage/components/AnnouncementBar.d.ts +7 -1
  191. package/dist/features/homepage/components/AnnouncementBar.js +3 -3
  192. package/dist/features/homepage/components/MarketplaceHomepageView.js +20 -6
  193. package/dist/features/homepage/hooks/homepage-query.d.ts +16 -0
  194. package/dist/features/homepage/hooks/homepage-query.js +45 -0
  195. package/dist/features/homepage/hooks/useFeaturedAuctions.d.ts +10 -0
  196. package/dist/features/homepage/hooks/useFeaturedAuctions.js +28 -8
  197. package/dist/features/homepage/hooks/useFeaturedPreOrders.js +17 -8
  198. package/dist/features/homepage/hooks/useFeaturedProducts.js +15 -8
  199. package/dist/features/homepage/schemas/index.d.ts +2 -2
  200. package/dist/features/layout/AppLayoutShell.d.ts +11 -1
  201. package/dist/features/layout/AppLayoutShell.js +15 -14
  202. package/dist/features/layout/BackToTop.d.ts +9 -0
  203. package/dist/features/layout/BackToTop.js +10 -1
  204. package/dist/features/layout/BottomActions.js +28 -25
  205. package/dist/features/layout/BottomChrome.d.ts +34 -0
  206. package/dist/features/layout/BottomChrome.js +55 -0
  207. package/dist/features/layout/BottomNavLayout.d.ts +6 -0
  208. package/dist/features/layout/BottomNavLayout.js +7 -1
  209. package/dist/features/layout/BottomNavbar.js +15 -2
  210. package/dist/features/layout/FooterLayout.js +11 -1
  211. package/dist/features/layout/NavbarLayout.js +1 -1
  212. package/dist/features/layout/TitleBarLayout.d.ts +7 -0
  213. package/dist/features/layout/TitleBarLayout.js +28 -5
  214. package/dist/features/layout/index.d.ts +2 -0
  215. package/dist/features/layout/index.js +1 -0
  216. package/dist/features/lottery/types/index.d.ts +3 -0
  217. package/dist/features/media/MediaPickerModal.js +5 -1
  218. package/dist/features/messages/schemas/index.d.ts +2 -2
  219. package/dist/features/orders/components/OrderScopeTabs.d.ts +20 -0
  220. package/dist/features/orders/components/OrderScopeTabs.js +31 -0
  221. package/dist/features/orders/constants/order-scope.d.ts +48 -0
  222. package/dist/features/orders/constants/order-scope.js +111 -0
  223. package/dist/features/orders/repository/orders.repository.d.ts +54 -7
  224. package/dist/features/orders/repository/orders.repository.js +116 -22
  225. package/dist/features/orders/schemas/firestore.d.ts +117 -0
  226. package/dist/features/orders/schemas/firestore.js +30 -0
  227. package/dist/features/orders/schemas/index.d.ts +18 -18
  228. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +14 -9
  229. package/dist/features/pre-orders/components/PreOrdersListView.js +6 -5
  230. package/dist/features/pre-orders/schemas/index.d.ts +1 -1
  231. package/dist/features/products/actions/product-actions.d.ts +14 -0
  232. package/dist/features/products/actions/product-actions.js +49 -11
  233. package/dist/features/products/api/route.d.ts +12 -0
  234. package/dist/features/products/api/route.js +42 -100
  235. package/dist/features/products/components/ArtStickersListView.js +2 -2
  236. package/dist/features/products/components/AuctionsIndexListing.js +13 -11
  237. package/dist/features/products/components/AvailabilityTabs.d.ts +11 -0
  238. package/dist/features/products/components/AvailabilityTabs.js +47 -0
  239. package/dist/features/products/components/MakeOfferButton.js +48 -49
  240. package/dist/features/products/components/PrizeDrawCollage.d.ts +20 -4
  241. package/dist/features/products/components/PrizeDrawCollage.js +9 -1
  242. package/dist/features/products/components/PrizeDrawsIndexListing.js +14 -12
  243. package/dist/features/products/components/PrizeDrawsListingView.js +2 -2
  244. package/dist/features/products/components/PrizeDrawsSection.d.ts +7 -1
  245. package/dist/features/products/components/PrizeDrawsSection.js +23 -18
  246. package/dist/features/products/components/ProductDetailPageView.d.ts +4 -1
  247. package/dist/features/products/components/ProductDetailPageView.js +18 -4
  248. package/dist/features/products/components/ProductForm.js +5 -2
  249. package/dist/features/products/components/ProductGrid.js +6 -1
  250. package/dist/features/products/components/ProductsIndexListing.js +22 -32
  251. package/dist/features/products/components/ProductsIndexPageView.js +6 -9
  252. package/dist/features/products/components/index.d.ts +2 -2
  253. package/dist/features/products/components/index.js +1 -1
  254. package/dist/features/products/config/listing-type-listing-config.d.ts +20 -0
  255. package/dist/features/products/config/listing-type-listing-config.js +22 -10
  256. package/dist/features/products/constants/action-defs.d.ts +2 -0
  257. package/dist/features/products/constants/action-defs.js +6 -0
  258. package/dist/features/products/hooks/useAvailabilityScope.d.ts +13 -0
  259. package/dist/features/products/hooks/useAvailabilityScope.js +19 -0
  260. package/dist/features/products/hooks/useProducts.d.ts +5 -0
  261. package/dist/features/products/hooks/useProducts.js +13 -2
  262. package/dist/features/products/repository/products.repository.d.ts +31 -2
  263. package/dist/features/products/repository/products.repository.js +53 -21
  264. package/dist/features/products/schemas/firestore.d.ts +17 -3
  265. package/dist/features/products/schemas/firestore.js +7 -2
  266. package/dist/features/products/schemas/index.d.ts +66 -66
  267. package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
  268. package/dist/features/products/types/index.d.ts +16 -1
  269. package/dist/features/promotions/schemas/index.d.ts +2 -2
  270. package/dist/features/reviews/schemas/index.d.ts +16 -16
  271. package/dist/features/search/schemas/index.d.ts +4 -4
  272. package/dist/features/seller/actions/offer-actions.js +25 -2
  273. package/dist/features/seller/components/SellerArtView.js +5 -0
  274. package/dist/features/seller/components/SellerAuctionsView.js +13 -15
  275. package/dist/features/seller/components/SellerClassifiedView.js +5 -0
  276. package/dist/features/seller/components/SellerDigitalCodesView.js +5 -0
  277. package/dist/features/seller/components/SellerLiveView.js +5 -0
  278. package/dist/features/seller/components/SellerOrdersView.js +31 -9
  279. package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -18
  280. package/dist/features/seller/components/SellerPreOrdersView.js +5 -0
  281. package/dist/features/seller/components/SellerPrizeDrawsView.js +14 -16
  282. package/dist/features/seller/components/SellerProductShell.js +7 -1
  283. package/dist/features/seller/components/SellerProductsView.js +11 -8
  284. package/dist/features/seller/components/SellerStickersView.js +5 -0
  285. package/dist/features/seller/components/index.d.ts +0 -2
  286. package/dist/features/seller/components/index.js +0 -1
  287. package/dist/features/seller/repository/offer.repository.d.ts +73 -16
  288. package/dist/features/seller/repository/offer.repository.js +38 -3
  289. package/dist/features/seller/schemas/index.d.ts +2 -2
  290. package/dist/features/seller/schemas/offer-forms.d.ts +25 -0
  291. package/dist/features/seller/schemas/offer-forms.js +21 -0
  292. package/dist/features/seller/server.d.ts +0 -1
  293. package/dist/features/seller/server.js +0 -1
  294. package/dist/features/shell/SectionForm.d.ts +99 -0
  295. package/dist/features/shell/SectionForm.js +128 -0
  296. package/dist/features/shell/entity-form.d.ts +109 -0
  297. package/dist/features/shell/entity-form.js +96 -0
  298. package/dist/features/shell/index.d.ts +4 -0
  299. package/dist/features/shell/index.js +2 -0
  300. package/dist/features/stores/api/[storeSlug]/route.js +2 -25
  301. package/dist/features/stores/components/StoreAuctionsListing.js +15 -1
  302. package/dist/features/stores/components/StoreClassifiedsListing.js +12 -2
  303. package/dist/features/stores/components/StoreDetailLayoutView.js +24 -71
  304. package/dist/features/stores/components/StoreDigitalCodesListing.js +12 -2
  305. package/dist/features/stores/components/StoreLiveItemsListing.js +12 -2
  306. package/dist/features/stores/components/StorePreOrdersListing.js +12 -1
  307. package/dist/features/stores/components/StoreProductsListing.js +11 -1
  308. package/dist/features/stores/repository/store.repository.js +6 -0
  309. package/dist/features/stores/schemas/index.d.ts +6 -6
  310. package/dist/features/support/schemas/index.d.ts +4 -4
  311. package/dist/features/tester/actions/checklist-item-actions.d.ts +4 -4
  312. package/dist/features/tester/seed-data/offers-tester-seed-data.js +119 -3
  313. package/dist/features/tester/seed-data/products-tester-seed-data.js +131 -1
  314. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +105 -1
  315. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  316. package/dist/index.d.ts +13 -10
  317. package/dist/index.js +14 -9
  318. package/dist/next/routing/route-map.d.ts +7 -1
  319. package/dist/next/routing/route-map.js +5 -0
  320. package/dist/providers/db-firebase/base.repository.d.ts +30 -0
  321. package/dist/providers/db-firebase/base.repository.js +50 -1
  322. package/dist/providers/db-firebase/index.d.ts +1 -1
  323. package/dist/providers/db-firebase/index.js +1 -1
  324. package/dist/providers/db-firebase/sieve.d.ts +25 -0
  325. package/dist/providers/db-firebase/sieve.js +52 -18
  326. package/dist/react/hooks/useUrlTable.js +10 -1
  327. package/dist/schemas/registry.d.ts +68 -68
  328. package/dist/seed/_helpers/category-forest.d.ts +2 -0
  329. package/dist/seed/_helpers/category-forest.js +309 -0
  330. package/dist/seed/_helpers/category-tree.d.ts +27 -0
  331. package/dist/seed/_helpers/category-tree.js +103 -0
  332. package/dist/seed/catalogue-seed-data.js +12 -12
  333. package/dist/seed/categories-seed-data.js +177 -162
  334. package/dist/seed/events-seed-data.js +61 -10
  335. package/dist/seed/homepage-sections-seed-data.js +5 -1
  336. package/dist/seed/offers-seed-data.js +32 -27
  337. package/dist/seed/products-art-seed-data.js +50 -12
  338. package/dist/seed/products-auctions-seed-data.js +97 -18
  339. package/dist/seed/products-classifieds-seed-data.js +80 -16
  340. package/dist/seed/products-digital-codes-seed-data.js +66 -18
  341. package/dist/seed/products-live-items-seed-data.js +64 -13
  342. package/dist/seed/products-preorders-seed-data.d.ts +0 -1
  343. package/dist/seed/products-preorders-seed-data.js +52 -13
  344. package/dist/seed/products-prize-draws-seed-data.js +63 -14
  345. package/dist/seed/products-standard-seed-data.js +99 -26
  346. package/dist/seed/products-stickers-seed-data.js +47 -12
  347. package/dist/seed/reviews-seed-data.js +6 -1
  348. package/dist/seed/site-settings-seed-data.d.ts +1 -38
  349. package/dist/seed/site-settings-seed-data.js +38 -76
  350. package/dist/seed/stores-seed-data.js +71 -1
  351. package/dist/seed/users-seed-data.js +15 -5
  352. package/dist/server-entry.d.ts +1 -2
  353. package/dist/server-entry.js +4 -3
  354. package/dist/server.d.ts +4 -3
  355. package/dist/server.js +8 -4
  356. package/dist/styles.css +78 -12
  357. package/dist/tailwind-utilities.css +1 -1
  358. package/dist/ui/components/Button.style.css +19 -4
  359. package/dist/ui/components/CollapsibleSection.d.ts +17 -1
  360. package/dist/ui/components/CollapsibleSection.js +6 -2
  361. package/dist/ui/components/FilterChipGroup.d.ts +25 -1
  362. package/dist/ui/components/FilterChipGroup.js +21 -4
  363. package/dist/ui/components/HandMode.style.css +47 -0
  364. package/dist/ui/components/Layout.d.ts +9 -2
  365. package/dist/ui/components/Layout.js +1 -1
  366. package/dist/ui/components/ListingLayout.js +8 -8
  367. package/dist/ui/components/ListingLayout.style.css +8 -7
  368. package/dist/ui/components/StickyToolbar.d.ts +3 -1
  369. package/dist/ui/components/StickyToolbar.js +62 -20
  370. package/dist/ui/components/bottom-chrome-slot.d.ts +21 -0
  371. package/dist/ui/components/bottom-chrome-slot.js +29 -0
  372. package/dist/ui/components/index.style.css +3 -0
  373. package/dist/ui/forms/FormShell.d.ts +43 -13
  374. package/dist/ui/forms/FormShell.js +22 -11
  375. package/dist/ui/forms/index.d.ts +1 -1
  376. package/dist/ui/index.d.ts +1 -1
  377. package/package.json +2 -2
@@ -10,13 +10,21 @@
10
10
  *
11
11
  * WHAT: `parsePublicProductParams` (URL/searchParams -> typed input) and
12
12
  * `listPublicProducts` (typed input -> page of documents). Firestore-safe
13
- * clauses go into the query; `inStock` and unsafe date ranges are applied as
14
- * in-memory predicates over one bounded fetch, then re-sorted into the caller's
15
- * requested order and re-paginated.
13
+ * clauses go into the query; the availability scope and unsafe date
14
+ * ranges are applied over bounded fetches, then re-sorted into the
15
+ * caller's requested order and re-paginated. See the comment block above
16
+ * `listPublicProducts` for why the three scopes execute differently.
17
+ *
18
+ * Since 2026-08-24 this also backs the admin/seller dashboards, via the
19
+ * `ANY_STATUS` sentinel — so a dashboard and a public page filtering the
20
+ * same listing type issue byte-identical queries instead of two
21
+ * hand-rolled filter builders that drift.
16
22
  *
17
23
  * EXPORTS:
18
24
  * PublicProductListInput, PublicProductListResult, PublicProductExecutor,
19
- * PublicProductListOptions, parsePublicProductParams, listPublicProducts
25
+ * PublicProductListOptions, parsePublicProductParams, listPublicProducts,
26
+ * listStoreProducts, defaultAvailabilityForListingTypes, ANY_STATUS,
27
+ * PUBLIC_PRODUCT_MAX_PAGE_SIZE
20
28
  *
21
29
  * @tag domain:products
22
30
  * @tag layer:server-data
@@ -33,11 +41,19 @@ import { TABLE_KEYS } from "../../../../constants/table-keys";
33
41
  import { sortBy } from "../../../../constants/sort";
34
42
  import { SIEVE_OP, sieveAnd, sieveFilter, expandSieveParam, } from "../../../../utils/sieve-builder";
35
43
  import { ALL_LISTING_TYPES } from "../../../shared/listing-types/feature-flags";
36
- import { hideDefaultsFor } from "../../../shared/listing-types/_registry";
44
+ import { isListingRowAvailable, unavailableClausesFor, } from "../../../shared/listing-types/_registry";
45
+ import { AVAILABILITY_VALUES, isAvailabilityFilter, } from "../../../../constants/field-names";
37
46
  /** Vercel Hobby Fluid Compute ceiling (CLAUDE.md Rule #6) — never fetch more at once. */
38
47
  export const PUBLIC_PRODUCT_MAX_PAGE_SIZE = 50;
39
48
  /**
40
- * Which "Show X" defaults apply to a set of listing types.
49
+ * `status` sentinel meaning "every publication state". Only the authenticated
50
+ * admin/seller dashboards may pass it — a public surface that did would leak
51
+ * drafts. A sentinel rather than `status: undefined` because undefined already
52
+ * means "use the safe default", and conflating the two is how a leak ships.
53
+ */
54
+ export const ANY_STATUS = "__any__";
55
+ /**
56
+ * The availability scope a listing surface starts in.
41
57
  *
42
58
  * THE POINT OF THIS FUNCTION is that SSR and the client refetch call the SAME
43
59
  * implementation. `staleTime: Infinity` freezes SSR `initialData` forever, so
@@ -46,21 +62,19 @@ export const PUBLIC_PRODUCT_MAX_PAGE_SIZE = 50;
46
62
  * them is wrong — permanently (Root Cause #30). Two mirrored literals is
47
63
  * exactly how that bug is written; one shared function is how it isn't.
48
64
  *
49
- * `/products` now spans all nine listing types, which makes this load-bearing
50
- * rather than theoretical: a mixed set includes both sold-out-able types
51
- * (products, art) and time-boxed ones (auctions, pre-orders, prize draws), so
52
- * BOTH toggles can apply at once.
65
+ * It is a constant today rather than a per-type decision — every listing
66
+ * surface opens on what a shopper can actually buy. It stays a function
67
+ * because that is the seam a surface would override through, and because
68
+ * `audit-listing-filter-parity` asserts on this name to prove every SSR view
69
+ * derives its default rather than hard-coding one (two of them used to).
53
70
  */
54
- export function defaultTogglesForListingTypes(types) {
55
- // No explicit set = every type is in play, so both defaults apply.
71
+ export function defaultAvailabilityForListingTypes(_types) {
72
+ return { availability: AVAILABILITY_VALUES.AVAILABLE };
73
+ }
74
+ /** Narrow arbitrary strings to real listing types, dropping the rest. */
75
+ function asListingTypes(types) {
56
76
  const effective = types && types.length > 0 ? types : ALL_LISTING_TYPES;
57
- const defaults = hideDefaultsFor(effective.filter((t) => ALL_LISTING_TYPES.includes(t)));
58
- return {
59
- hideSoldByDefault: defaults.includes("sold"),
60
- // "ended" and "closed" are the same query mechanic (a date range against
61
- // the type's own end field); they differ only in the toggle's wording.
62
- hideEndedByDefault: defaults.includes("ended") || defaults.includes("closed"),
63
- };
77
+ return effective.filter((t) => ALL_LISTING_TYPES.includes(t));
64
78
  }
65
79
  const DEFAULT_PAGE = 1;
66
80
  const DEFAULT_PAGE_SIZE = 24;
@@ -114,6 +128,29 @@ function pipeList(raw) {
114
128
  const parts = raw.split("|").map((s) => s.trim()).filter(Boolean);
115
129
  return parts.length > 0 ? parts : undefined;
116
130
  }
131
+ /**
132
+ * Read the availability scope, with a back-compat mapping for the three URL
133
+ * spellings this replaced.
134
+ *
135
+ * An unrecognised token falls back to the caller's default rather than being
136
+ * passed through — a raw query param must never reach a Firestore value.
137
+ */
138
+ function readAvailability(get, fallback) {
139
+ const raw = get(TABLE_KEYS.AVAILABILITY);
140
+ if (raw && isAvailabilityFilter(raw))
141
+ return raw;
142
+ if (raw)
143
+ return fallback;
144
+ // `showSold` / `showEnded` / `showClosed` each meant "stop filtering", so
145
+ // they map to `all`, NOT to `unavailable`. Kept only so links shared before
146
+ // 2026-08-24 still resolve to something sensible; nothing writes them.
147
+ if (get(TABLE_KEYS.SHOW_SOLD) === "true" ||
148
+ get(TABLE_KEYS.SHOW_ENDED) === "true" ||
149
+ get(TABLE_KEYS.SHOW_CLOSED) === "true") {
150
+ return AVAILABILITY_VALUES.ALL;
151
+ }
152
+ return fallback;
153
+ }
117
154
  /**
118
155
  * Read the shared public-listing query params. SSR views hand in Next's
119
156
  * `searchParams` record; the API route hands in `url.searchParams`. Both produce
@@ -160,21 +197,12 @@ export function parsePublicProductParams(params, defaults) {
160
197
  typeFacets: readTypeFacets(get),
161
198
  preOrderProductionStatus: get(TABLE_KEYS.PREORDER_STATUS) || get("preOrderStatus") || undefined,
162
199
  prizeRevealStatus: get(TABLE_KEYS.PRIZE_REVEAL_STATUS) || undefined,
163
- // Three spellings of one intent — /products and /art call it "Show sold",
164
- // /pre-orders calls it "Show closed", and the client hook sends `inStock`.
165
- // Any of them turning the filter OFF wins, so SSR and the client refetch
166
- // agree on what the default view means (Root Cause #30).
167
- inStock: get(TABLE_KEYS.SHOW_SOLD) === "true" || get(TABLE_KEYS.SHOW_CLOSED) === "true"
168
- ? undefined
169
- : get(TABLE_KEYS.IN_STOCK)
170
- ? get(TABLE_KEYS.IN_STOCK) === "true"
171
- : defaults?.hideSoldByDefault
172
- ? true
173
- : undefined,
174
- dateFrom: get(TABLE_KEYS.DATE_FROM) ||
175
- (defaults?.hideEndedByDefault && get(TABLE_KEYS.SHOW_ENDED) !== "true"
176
- ? new Date().toISOString()
177
- : undefined),
200
+ availability: readAvailability(get, defaults?.availability),
201
+ // Now a plain drawer facet again. It used to double as the hide-ended
202
+ // mechanism, which is why it only worked when exactly one listing type was
203
+ // selected — `dateFieldFor` returns null otherwise, so /products silently
204
+ // showed ended auctions in its default view.
205
+ dateFrom: get(TABLE_KEYS.DATE_FROM) || undefined,
178
206
  dateTo: get(TABLE_KEYS.DATE_TO) || undefined,
179
207
  page: num(get(TABLE_KEYS.PAGE)) ?? DEFAULT_PAGE,
180
208
  pageSize: num(get(TABLE_KEYS.PAGE_SIZE)) ?? defaults?.pageSize ?? DEFAULT_PAGE_SIZE,
@@ -196,7 +224,10 @@ function buildFirestoreSafeFilters(input) {
196
224
  const parts = [];
197
225
  // Published-only unless the caller explicitly asks otherwise. Without this
198
226
  // default, any refetch that omits `status` leaks drafts (Root Cause #30).
199
- parts.push(sieveFilter(PRODUCT_FIELDS.STATUS, SIEVE_OP.EQ, input.status || PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED));
227
+ // ANY_STATUS is the one opt-out, reserved for the authenticated dashboards.
228
+ if (input.status !== ANY_STATUS) {
229
+ parts.push(sieveFilter(PRODUCT_FIELDS.STATUS, SIEVE_OP.EQ, input.status || PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED));
230
+ }
200
231
  const types = input.listingTypes?.filter(Boolean) ?? [];
201
232
  if (types.length > 0) {
202
233
  parts.push(sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, types.join("|")));
@@ -308,72 +339,188 @@ async function defaultExecutor(query) {
308
339
  cursor: null,
309
340
  };
310
341
  }
342
+ /** Render one `UnavailableClause` as a Sieve clause against a fixed `now`. */
343
+ function clauseToSieve(clause, now) {
344
+ const value = clause.value === "NOW" ? now.toISOString() : clause.value;
345
+ return clause.op === "lt"
346
+ ? sieveFilter(clause.field, SIEVE_OP.LT, value)
347
+ : sieveFilter(clause.field, SIEVE_OP.EQ, value);
348
+ }
311
349
  // ---------------------------------------------------------------------------
312
350
  // The one public listing query
313
351
  // ---------------------------------------------------------------------------
352
+ //
353
+ // AVAILABILITY IS ASYMMETRIC, and the asymmetry is what makes this tractable.
354
+ //
355
+ // "unavailable" is SPARSE and expressible as an OR of EQUALITIES — sold out,
356
+ // ended, closed, depleted. Firestore cannot OR across different fields, but
357
+ // it can run each equality as its own bounded query, and merging N small
358
+ // result sets in memory is cheap. That is Path C.
359
+ //
360
+ // "available" is DENSE and NEGATION-shaped: NOT(a OR b OR c). There is no
361
+ // query for that, so it stays what it has always been — one bounded window
362
+ // in the caller's own sort order, filtered per row. That is fine precisely
363
+ // because it is dense: nearly every row in the window passes.
364
+ //
365
+ // Path A ("all") applies no predicate at all and is therefore the only scope
366
+ // with true Firestore pagination and an exact total.
314
367
  export async function listPublicProducts(input, opts) {
368
+ const now = opts?.now ?? new Date();
315
369
  const page = Math.max(1, input.page ?? DEFAULT_PAGE);
316
370
  const pageSize = Math.min(PUBLIC_PRODUCT_MAX_PAGE_SIZE, Math.max(1, input.pageSize ?? DEFAULT_PAGE_SIZE));
317
371
  const sorts = input.sorts || DEFAULT_SORTS;
318
372
  const requestedSortField = sorts.replace(/^-/, "");
373
+ const sortDesc = sorts.startsWith("-");
374
+ const availability = input.availability ?? AVAILABILITY_VALUES.ALL;
375
+ const wantAvailable = availability === AVAILABILITY_VALUES.AVAILABLE;
376
+ const wantUnavailable = availability === AVAILABILITY_VALUES.UNAVAILABLE;
319
377
  const dateField = dateFieldFor(input.listingTypes);
320
378
  const hasDateRange = Boolean((input.dateFrom || input.dateTo) && dateField);
321
- // Firestore DOES accept one inequality when the query sorts by that same field —
322
- // the auctions "Ending Soon" default is exactly that shape, so push it down.
323
- const canPushDate = hasDateRange && !input.inStock && requestedSortField === dateField;
379
+ // Firestore DOES accept one inequality when the query sorts by that same
380
+ // field, so a matching date facet can still be pushed down.
381
+ const canPushDate = hasDateRange && !wantAvailable && requestedSortField === dateField;
324
382
  const safeFilters = buildFirestoreSafeFilters(input);
325
- const filters = sieveAnd(safeFilters, ...(canPushDate && input.dateFrom
326
- ? [sieveFilter(dateField, SIEVE_OP.GTE, input.dateFrom)]
327
- : []), ...(canPushDate && input.dateTo
328
- ? [sieveFilter(dateField, SIEVE_OP.LTE, input.dateTo)]
329
- : []));
330
- const hasUnsafeFilter = Boolean(input.inStock) || (hasDateRange && !canPushDate);
383
+ const dateClauses = [
384
+ ...(canPushDate && input.dateFrom
385
+ ? [sieveFilter(dateField, SIEVE_OP.GTE, input.dateFrom)]
386
+ : []),
387
+ ...(canPushDate && input.dateTo
388
+ ? [sieveFilter(dateField, SIEVE_OP.LTE, input.dateTo)]
389
+ : []),
390
+ ];
391
+ const executor = opts?.executor ?? defaultExecutor;
392
+ const types = asListingTypes(input.listingTypes);
393
+ // ── Path B: one type whose unavailability IS a pushable inequality, and the
394
+ // caller is already sorting by that field. Ended auctions sorted by end
395
+ // date is the whole reason this branch exists — it gives the archive real,
396
+ // unbounded-depth Firestore pagination instead of a 50-row window.
397
+ const pushdown = wantUnavailable && types.length === 1 && !hasDateRange
398
+ ? unavailableClausesFor(types).find((c) => c.op === "lt" && c.sortField === requestedSortField)
399
+ : undefined;
400
+ if (pushdown) {
401
+ const filters = sieveAnd(safeFilters, clauseToSieve(pushdown, now));
402
+ const result = await runQuery(executor, {
403
+ filters,
404
+ sorts,
405
+ page,
406
+ pageSize,
407
+ cursor: input.cursor ?? null,
408
+ });
409
+ if (!result)
410
+ return null;
411
+ return finish({
412
+ items: result.items,
413
+ total: result.total,
414
+ page: result.page,
415
+ pageSize,
416
+ totalPages: result.totalPages,
417
+ hasMore: result.hasMore,
418
+ cursor: result.cursor ?? null,
419
+ truncated: false,
420
+ filters,
421
+ sorts,
422
+ enabled: opts?.enabledListingTypes,
423
+ });
424
+ }
425
+ // ── Path C: unavailable, everything else. One bounded query per distinct
426
+ // clause, run in parallel, merged and deduped. Nine listing types collapse
427
+ // to five clauses, so this is 5 × 50 = 250 documents in a single round —
428
+ // well inside Rule #6, and the same fan-out shape `computeRelatedItems`
429
+ // already uses.
430
+ if (wantUnavailable) {
431
+ const clauses = unavailableClausesFor(types);
432
+ const results = await Promise.all(clauses.map((clause) => {
433
+ // An inequality still has to be ordered by its own field, even here.
434
+ const clauseSorts = clause.op === "lt" && clause.sortField
435
+ ? sortBy(clause.sortField, "DESC")
436
+ : sorts;
437
+ return runQuery(executor, {
438
+ filters: sieveAnd(safeFilters, clauseToSieve(clause, now)),
439
+ sorts: clauseSorts,
440
+ page: 1,
441
+ pageSize: PUBLIC_PRODUCT_MAX_PAGE_SIZE,
442
+ cursor: null,
443
+ });
444
+ }));
445
+ // One failed branch must not blank the whole scope — but it is already
446
+ // logged loudly by `runQuery`, and it does mean the merge is incomplete,
447
+ // which `truncated` communicates honestly.
448
+ if (results.every((r) => r === null))
449
+ return null;
450
+ const merged = new Map();
451
+ let truncated = false;
452
+ for (const result of results) {
453
+ if (!result) {
454
+ truncated = true;
455
+ continue;
456
+ }
457
+ const rows = result.items;
458
+ if (rows.length >= PUBLIC_PRODUCT_MAX_PAGE_SIZE)
459
+ truncated = true;
460
+ for (const row of rows) {
461
+ const id = typeof row.id === "string" ? row.id : JSON.stringify(row.id);
462
+ if (!merged.has(id))
463
+ merged.set(id, row);
464
+ }
465
+ }
466
+ // Drop false positives — a clause selects a SUPERSET. A pre-order can hold
467
+ // `availableQuantity: 0` from an over-signed allocation while still open,
468
+ // and only the per-type predicate knows that.
469
+ const rows = [...merged.values()].filter((row) => !isListingRowAvailable(row, now));
470
+ const ordered = sortRows(rows, requestedSortField, sortDesc);
471
+ const start = (page - 1) * pageSize;
472
+ return finish({
473
+ items: ordered.slice(start, start + pageSize),
474
+ total: ordered.length,
475
+ page,
476
+ pageSize,
477
+ totalPages: truncated
478
+ ? page + 1
479
+ : Math.max(1, Math.ceil(ordered.length / pageSize)),
480
+ hasMore: truncated || start + pageSize < ordered.length,
481
+ cursor: null,
482
+ truncated,
483
+ filters: clauses.map((c) => sieveAnd(safeFilters, clauseToSieve(c, now))).join(" OR "),
484
+ sorts,
485
+ enabled: opts?.enabledListingTypes,
486
+ });
487
+ }
488
+ // ── Path A ("all") and the dense "available" case share one query. The only
489
+ // difference is whether a per-row predicate runs afterwards.
490
+ const filters = sieveAnd(safeFilters, ...dateClauses);
491
+ const inMemory = wantAvailable || (hasDateRange && !canPushDate);
331
492
  // When a date range stays in memory, fetch in whichever direction front-loads
332
493
  // the rows that will PASS it: `dateFrom` (">=", still live) wants the most
333
494
  // future dates first; `dateTo` ("<=", ends by) wants the earliest first.
334
495
  // Fetching in the client's own order instead is what made live auctions
335
496
  // invisible once ~50 had already ended.
336
- const fetchSorts = hasUnsafeFilter && hasDateRange && dateField
497
+ const fetchSorts = inMemory && hasDateRange && !canPushDate && dateField
337
498
  ? input.dateFrom
338
499
  ? sortBy(dateField, "DESC")
339
500
  : sortBy(dateField, "ASC")
340
501
  : sorts;
341
- const executor = opts?.executor ?? defaultExecutor;
342
- let result;
343
- try {
344
- result = await executor({
345
- filters,
346
- sorts: fetchSorts,
347
- page: hasUnsafeFilter ? 1 : page,
348
- pageSize: hasUnsafeFilter ? PUBLIC_PRODUCT_MAX_PAGE_SIZE : pageSize,
349
- cursor: hasUnsafeFilter ? null : (input.cursor ?? null),
350
- });
351
- }
352
- catch (error) {
353
- void normalizeError(error);
354
- // Loud. A swallowed query failure here is indistinguishable from an empty
355
- // catalogue at every call site above, which is exactly how the /art bug hid.
356
- serverLogger.error("listPublicProducts query failed", {
357
- filters,
358
- sorts: fetchSorts,
359
- error: error instanceof Error ? error.message : String(error),
360
- });
502
+ const result = await runQuery(executor, {
503
+ filters,
504
+ sorts: fetchSorts,
505
+ page: inMemory ? 1 : page,
506
+ pageSize: inMemory ? PUBLIC_PRODUCT_MAX_PAGE_SIZE : pageSize,
507
+ cursor: inMemory ? null : (input.cursor ?? null),
508
+ });
509
+ if (!result)
361
510
  return null;
362
- }
363
511
  let items = result.items;
364
512
  let total = result.total;
365
513
  let totalPages = result.totalPages;
366
514
  let resultPage = result.page;
367
515
  let hasMore = result.hasMore;
368
516
  let cursor = result.cursor ?? null;
369
- if (hasUnsafeFilter) {
517
+ let truncated = false;
518
+ if (inMemory) {
519
+ truncated = items.length >= PUBLIC_PRODUCT_MAX_PAGE_SIZE;
370
520
  const filtered = items.filter((item) => {
371
- if (input.inStock) {
372
- const stock = item[PRODUCT_FIELDS.STOCK_QUANTITY];
373
- if (!(typeof stock === "number" && stock > 0))
374
- return false;
375
- }
376
- if (hasDateRange && dateField) {
521
+ if (wantAvailable && !isListingRowAvailable(item, now))
522
+ return false;
523
+ if (hasDateRange && !canPushDate && dateField) {
377
524
  const raw = item[dateField];
378
525
  if (!raw)
379
526
  return false;
@@ -385,28 +532,16 @@ export async function listPublicProducts(input, opts) {
385
532
  }
386
533
  return true;
387
534
  });
388
- const ordered = fetchSorts === sorts ? filtered : sortRows(filtered, requestedSortField, sorts.startsWith("-"));
535
+ const ordered = fetchSorts === sorts ? filtered : sortRows(filtered, requestedSortField, sortDesc);
389
536
  total = ordered.length;
390
- totalPages = Math.max(1, Math.ceil(total / pageSize));
537
+ totalPages = truncated ? page + 1 : Math.max(1, Math.ceil(total / pageSize));
391
538
  resultPage = page;
392
539
  const start = (page - 1) * pageSize;
393
540
  items = ordered.slice(start, start + pageSize);
394
- hasMore = start + pageSize < total;
541
+ hasMore = truncated || start + pageSize < total;
395
542
  cursor = null;
396
543
  }
397
- // Feature-flagged-off types never reach a public surface.
398
- const enabled = opts?.enabledListingTypes;
399
- if (enabled && enabled.size > 0) {
400
- const before = items.length;
401
- items = items.filter((it) => {
402
- const lt = typeof it.listingType === "string" ? it.listingType : "standard";
403
- return enabled.has(lt);
404
- });
405
- const removed = before - items.length;
406
- if (removed > 0)
407
- total = Math.max(0, total - removed);
408
- }
409
- return {
544
+ return finish({
410
545
  items,
411
546
  total,
412
547
  page: resultPage,
@@ -414,8 +549,49 @@ export async function listPublicProducts(input, opts) {
414
549
  totalPages,
415
550
  hasMore,
416
551
  cursor,
552
+ truncated,
417
553
  filters,
418
554
  sorts,
555
+ enabled: opts?.enabledListingTypes,
556
+ });
557
+ }
558
+ /**
559
+ * Run one executor call, logging loudly and returning null on failure.
560
+ *
561
+ * Loud on purpose: a swallowed query failure is indistinguishable from an
562
+ * empty catalogue at every call site above, which is exactly how the /art bug
563
+ * hid for months (Root Cause #59).
564
+ */
565
+ async function runQuery(executor, query) {
566
+ try {
567
+ return await executor(query);
568
+ }
569
+ catch (error) {
570
+ void normalizeError(error);
571
+ serverLogger.error("listPublicProducts query failed", {
572
+ filters: query.filters,
573
+ sorts: query.sorts,
574
+ error: error instanceof Error ? error.message : String(error),
575
+ });
576
+ return null;
577
+ }
578
+ }
579
+ /** Apply the feature-flag post-filter and assemble the result. */
580
+ function finish(draft) {
581
+ const { enabled, ...result } = draft;
582
+ if (!enabled || enabled.size === 0)
583
+ return result;
584
+ // Feature-flagged-off types never reach a public surface.
585
+ const before = result.items.length;
586
+ const items = result.items.filter((it) => {
587
+ const lt = typeof it.listingType === "string" ? it.listingType : "standard";
588
+ return enabled.has(lt);
589
+ });
590
+ const removed = before - items.length;
591
+ return {
592
+ ...result,
593
+ items,
594
+ total: removed > 0 ? Math.max(0, result.total - removed) : result.total,
419
595
  };
420
596
  }
421
597
  /**
@@ -438,7 +614,7 @@ export async function listStoreProducts(storeId, listingTypes, searchParams = {}
438
614
  listingTypes,
439
615
  pageSize: defaults?.pageSize ?? DEFAULT_PAGE_SIZE,
440
616
  sorts: defaults?.sorts,
441
- ...defaultTogglesForListingTypes(listingTypes),
617
+ ...defaultAvailabilityForListingTypes(listingTypes),
442
618
  }),
443
619
  // The route owns the store identity — a `?storeId=` in the URL must not be
444
620
  // able to make one store's tab render another store's inventory.
@@ -0,0 +1,44 @@
1
+ import type { ListingTab } from "../../../../features/products/constants/listing-tabs";
2
+ /**
3
+ * Which slice of the catalogue a tab bar is counting. Exactly one of the three
4
+ * scoping fields is set in practice, but the shape is shared so all three
5
+ * surfaces go through one code path.
6
+ */
7
+ export interface ListingTabScope {
8
+ /**
9
+ * Category ids to match against `categorySlugs`. Products are tagged with
10
+ * their FULL ancestor chain, so this should be the page's own id alone —
11
+ * expanding descendants is both unnecessary and, past 30 entries, illegal
12
+ * (see `ProductRepository.MAX_ARRAY_CONTAINS_ANY`).
13
+ */
14
+ categoriesIn?: string[];
15
+ /**
16
+ * Brand DISPLAY NAME, not slug. `BrandDetailPageView` matches products on the
17
+ * free-text `brand` field, so the count must use the same field or it would
18
+ * disagree with the rows the tab then shows.
19
+ */
20
+ brandName?: string;
21
+ storeId?: string;
22
+ /** Publication state to count. Defaults to `published`. */
23
+ status?: string;
24
+ }
25
+ /**
26
+ * tabSlug -> number of rows, or `undefined` when that count could not be
27
+ * resolved.
28
+ *
29
+ * `undefined` is load-bearing: callers hide a tab at zero, so a failed query
30
+ * that reported 0 would hide a tab holding real inventory. Callers must treat
31
+ * `undefined` as "keep the tab visible" (Root Cause #59).
32
+ */
33
+ export type ListingTabCounts = Record<string, number | undefined>;
34
+ /**
35
+ * Count every tab in `tabs` within `scope`, in one parallel batch.
36
+ *
37
+ * The listing-type buckets come from each tab's own `listingType` field — which
38
+ * already carries `"art|stickers"` as a pipe OR-group — so this function never
39
+ * enumerates the `ListingType` union itself and cannot drift from it.
40
+ *
41
+ * `stores` is not counted here: only the category page has that tab and it
42
+ * already resolves its store list eagerly to render the tab body.
43
+ */
44
+ export declare function listingTabCounts(tabs: readonly ListingTab[], scope: ListingTabScope): Promise<ListingTabCounts>;
@@ -0,0 +1,97 @@
1
+ /*
2
+ * WHY: Category, brand and store detail pages each show a listing-type tab bar
3
+ * and hide a tab whose scope holds nothing. Before this module they each
4
+ * hand-wrote their own per-type count map, and all three had drifted:
5
+ * category and brand counted 6 of 10 tabs (classifieds / digital-codes /
6
+ * live / art fell through to `undefined` and could therefore NEVER hide),
7
+ * while the store page counted all 9 through eleven copy-pasted queries.
8
+ * That is Recurrent Root Cause #61 — an enumeration of `ListingType` kept
9
+ * by hand in more than one place always drifts.
10
+ * WHAT: One function that derives the buckets it counts from the SAME tab array
11
+ * the tab bar renders, so a tenth listing type is counted automatically.
12
+ *
13
+ * EXPORTS:
14
+ * ListingTabScope — the surface's scope (category / brand / store)
15
+ * ListingTabCounts — tabSlug -> count, `undefined` meaning "query failed"
16
+ * listingTabCounts — resolve every tab's count in one parallel batch
17
+ *
18
+ * @tag domain:products,categories,stores
19
+ * @tag layer:server
20
+ * @tag pattern:derived-from-registry
21
+ * @tag access:server-only
22
+ * @tag consumers:CategoryDetailPageView,BrandDetailPageView,StoreDetailLayoutView
23
+ * @tag sideEffects:none
24
+ */
25
+ import { categoriesRepository, productRepository } from "../../../../repositories";
26
+ import { serverLogger } from "../../../../monitoring";
27
+ import { normalizeError } from "../../../../errors/normalize";
28
+ const DEFAULT_STATUS = "published";
29
+ /** Bundles a category owns, resolved by the same rule the bundles tab renders. */
30
+ async function countBundles(scope) {
31
+ try {
32
+ const rows = await categoriesRepository.listByType("bundle", {
33
+ activeOnly: true,
34
+ limit: 100,
35
+ });
36
+ return rows.filter((row) => bundleMatchesScope(row, scope)).length;
37
+ }
38
+ catch (error) {
39
+ void normalizeError(error);
40
+ serverLogger.warn("listingTabCounts: bundle count failed; tab stays visible");
41
+ return undefined;
42
+ }
43
+ }
44
+ /**
45
+ * Does a bundle belong to the scope being counted?
46
+ *
47
+ * A bundle row carries `brandSlug` and `createdByStoreId` but has no category
48
+ * field of its own, so category membership is derived from the categories its
49
+ * MEMBER products are filed under — mirrored onto `bundleCategorySlugs` for
50
+ * index-friendly reads. Per Root Cause #42 the mirror is never trusted alone:
51
+ * when it is absent the bundle is treated as unscoped rather than as excluded,
52
+ * because silently dropping a bundle whose mirror one write path forgot is the
53
+ * exact failure that entry documents.
54
+ */
55
+ function bundleMatchesScope(bundle, scope) {
56
+ if (scope.storeId)
57
+ return bundle.createdByStoreId === scope.storeId;
58
+ if (scope.brandName)
59
+ return true; // brand pages filter by slug at the call site
60
+ if (scope.categoriesIn && scope.categoriesIn.length > 0) {
61
+ const mirror = bundle.bundleCategorySlugs;
62
+ if (!mirror || mirror.length === 0)
63
+ return true;
64
+ return mirror.some((slug) => scope.categoriesIn.includes(slug));
65
+ }
66
+ return true;
67
+ }
68
+ /**
69
+ * Count every tab in `tabs` within `scope`, in one parallel batch.
70
+ *
71
+ * The listing-type buckets come from each tab's own `listingType` field — which
72
+ * already carries `"art|stickers"` as a pipe OR-group — so this function never
73
+ * enumerates the `ListingType` union itself and cannot drift from it.
74
+ *
75
+ * `stores` is not counted here: only the category page has that tab and it
76
+ * already resolves its store list eagerly to render the tab body.
77
+ */
78
+ export async function listingTabCounts(tabs, scope) {
79
+ const listingTabs = tabs.filter((tab) => Boolean(tab.listingType));
80
+ const wantsBundles = tabs.some((tab) => tab.collection === "bundles");
81
+ const [typeCounts, bundleCount] = await Promise.all([
82
+ productRepository.countByListingTypes(listingTabs.map((tab) => String(tab.listingType)), {
83
+ status: scope.status ?? DEFAULT_STATUS,
84
+ storeId: scope.storeId,
85
+ categoriesIn: scope.categoriesIn,
86
+ brandName: scope.brandName,
87
+ }),
88
+ wantsBundles ? countBundles(scope) : Promise.resolve(undefined),
89
+ ]);
90
+ const counts = {};
91
+ for (const tab of listingTabs) {
92
+ counts[tab.id] = typeCounts[String(tab.listingType)];
93
+ }
94
+ if (wantsBundles)
95
+ counts.bundles = bundleCount;
96
+ return counts;
97
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Site-settings adapter — the allow-list projection for `GET /api/site-settings`.
3
+ *
4
+ * Every field of `SiteSettingsDocument` is triaged into exactly one of
5
+ * `PUBLIC_SITE_SETTINGS_FIELDS` (mapped into the returned literal) or
6
+ * `PRIVATE_SITE_SETTINGS_FIELDS` (with a stated reason). A field added to the
7
+ * schema and to neither list fails
8
+ * `scripts/audit-public-projection-parity.mjs` — new fields are private by
9
+ * default.
10
+ *
11
+ * Why this replaced a deny-list (2026-08-24): the route used to spread the
12
+ * document and delete three keys (`credentials`, `emailSettings`,
13
+ * `legalPages`), which meant everything else was public by default and
14
+ * edge-cached for 300s. That shipped `gst.gstin` + the registered legal name
15
+ * and address, all 25 `commissions.*` fields, `laborRate.hourlyRate`,
16
+ * `emi.surchargeSellerSharePercent`, `featureFlags.adminCheckoutBypass`, and —
17
+ * because they weren't even declared on the interface —
18
+ * `adSettings.providerCredentials` (which the ADMIN ads endpoint bothers to
19
+ * mask) plus every draft/paused/scheduled ad in `adSettings.inventory`.
20
+ *
21
+ * That last part is the argument for an allow-list over any amount of
22
+ * deny-list discipline: a deny-list keyed on a TypeScript interface is blind
23
+ * to fields the interface never declared. This function can only emit what it
24
+ * names.
25
+ *
26
+ * NOTE: fees are deliberately NOT exposed here. Cart and checkout receive them
27
+ * as SSR props via `toBuyerFacingFees`. If a client ever needs them from this
28
+ * endpoint, spread `toBuyerFacingFees(doc.commissions)` into the literal below
29
+ * and move `commissions` across to the public list — do not hand-roll a second
30
+ * subset.
31
+ */
32
+ import type { SiteSettingsDocument } from "../../../../features/admin/schemas/firestore";
33
+ import type { MediaVideoWatermark } from "../../../../features/media/MediaVideo";
34
+ import type { PublicSiteSettings } from "../../../shared/features/site-settings/types";
35
+ /**
36
+ * Document fields with a proven `useSiteSettings()` reader. Every entry below
37
+ * was traced to a call site; anything without one belongs in the private list,
38
+ * because an unused public field is pure attack surface.
39
+ */
40
+ export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "featureFlags", "notificationChannels", "announcementBar", "navConfig", "actionConfig", "background", "watermark"];
41
+ /** Everything else, with the reason it stays server-side. */
42
+ export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "disabledRoutes", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults"];
43
+ export interface ToPublicSiteSettingsOptions {
44
+ /**
45
+ * The resolved marker → wordmark → text watermark chain. Injected because
46
+ * the resolver lives in the consumer app
47
+ * (`src/lib/watermark/resolve-effective-watermark.ts`), not in appkit.
48
+ */
49
+ effectiveWatermark?: MediaVideoWatermark;
50
+ }
51
+ export declare function toPublicSiteSettings(doc: SiteSettingsDocument, opts?: ToPublicSiteSettingsOptions): PublicSiteSettings;