@mohasinac/appkit 4.53.0 → 4.53.2

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 (520) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/scaffolds/AppShell.js +1 -1
  3. package/dist/_internal/client/theme/ThemeProvider.js +15 -1
  4. package/dist/_internal/server/features/auctions/og.d.ts +3 -1
  5. package/dist/_internal/server/features/auctions/og.js +3 -1
  6. package/dist/_internal/server/features/bundles/actions.js +1 -1
  7. package/dist/_internal/server/features/business-documents/download.d.ts +41 -0
  8. package/dist/_internal/server/features/business-documents/download.js +91 -0
  9. package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
  10. package/dist/_internal/server/features/categories/adapters.js +10 -0
  11. package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
  12. package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
  13. package/dist/_internal/server/features/checkout/actions.d.ts +14 -0
  14. package/dist/_internal/server/features/checkout/actions.js +106 -12
  15. package/dist/_internal/server/features/checkout/bundle-expansion.js +17 -0
  16. package/dist/_internal/server/features/checkout/data.d.ts +22 -0
  17. package/dist/_internal/server/features/checkout/group-rule-revalidation.d.ts +34 -0
  18. package/dist/_internal/server/features/checkout/group-rule-revalidation.js +124 -0
  19. package/dist/_internal/server/features/checkout/supplier-identity.d.ts +55 -0
  20. package/dist/_internal/server/features/checkout/supplier-identity.js +81 -0
  21. package/dist/_internal/server/features/classified/metadata.js +1 -1
  22. package/dist/_internal/server/features/classified/og.d.ts +3 -1
  23. package/dist/_internal/server/features/classified/og.js +3 -1
  24. package/dist/_internal/server/features/digital-code/metadata.js +1 -1
  25. package/dist/_internal/server/features/digital-code/og.d.ts +3 -1
  26. package/dist/_internal/server/features/digital-code/og.js +3 -1
  27. package/dist/_internal/server/features/digital-code/pool.d.ts +19 -4
  28. package/dist/_internal/server/features/digital-code/pool.js +19 -4
  29. package/dist/_internal/server/features/live/metadata.js +1 -1
  30. package/dist/_internal/server/features/live/og.d.ts +3 -1
  31. package/dist/_internal/server/features/live/og.js +3 -1
  32. package/dist/_internal/server/features/messages/actions.js +1 -1
  33. package/dist/_internal/server/features/parts/og.js +1 -1
  34. package/dist/_internal/server/features/pre-orders/og.d.ts +3 -1
  35. package/dist/_internal/server/features/pre-orders/og.js +3 -1
  36. package/dist/_internal/server/features/price-index/market.d.ts +57 -0
  37. package/dist/_internal/server/features/price-index/market.js +120 -0
  38. package/dist/_internal/server/features/price-index/search.d.ts +18 -0
  39. package/dist/_internal/server/features/price-index/search.js +47 -7
  40. package/dist/_internal/server/features/prize-draws/og.d.ts +3 -1
  41. package/dist/_internal/server/features/prize-draws/og.js +3 -1
  42. package/dist/_internal/server/features/products/group-members.d.ts +26 -0
  43. package/dist/_internal/server/features/products/group-members.js +7 -1
  44. package/dist/_internal/server/features/products/list-public.d.ts +12 -0
  45. package/dist/_internal/server/features/products/list-public.js +10 -1
  46. package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
  47. package/dist/_internal/server/features/products/listing-tab-counts.js +128 -10
  48. package/dist/_internal/server/features/products/og.d.ts +3 -1
  49. package/dist/_internal/server/features/products/og.js +3 -1
  50. package/dist/_internal/server/features/search/scoped-sources.d.ts +26 -0
  51. package/dist/_internal/server/features/search/scoped-sources.js +252 -0
  52. package/dist/_internal/server/features/search/search-everything.d.ts +57 -0
  53. package/dist/_internal/server/features/search/search-everything.js +70 -20
  54. package/dist/_internal/server/features/seo/sitemap.js +6 -0
  55. package/dist/_internal/server/features/shared/listing-adapters.js +4 -2
  56. package/dist/_internal/server/features/sticker-trace/adapters.d.ts +11 -0
  57. package/dist/_internal/server/features/sticker-trace/adapters.js +1 -0
  58. package/dist/_internal/server/features/sticker-trace/promote-to-listings.d.ts +26 -0
  59. package/dist/_internal/server/features/sticker-trace/promote-to-listings.js +264 -0
  60. package/dist/_internal/server/features/sticker-trace/sheet-actions.d.ts +3 -1
  61. package/dist/_internal/server/features/sticker-trace/sheet-actions.js +32 -0
  62. package/dist/_internal/server/features/sticker-trace/storage.d.ts +20 -1
  63. package/dist/_internal/server/features/sticker-trace/storage.js +20 -1
  64. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  65. package/dist/_internal/server/features/stores/adapters.js +16 -0
  66. package/dist/_internal/server/features/stores/index.d.ts +1 -0
  67. package/dist/_internal/server/features/stores/index.js +1 -0
  68. package/dist/_internal/server/features/stores/tabs.d.ts +30 -0
  69. package/dist/_internal/server/features/stores/tabs.js +124 -0
  70. package/dist/_internal/shared/checkout/rules/_defaults.js +0 -2
  71. package/dist/_internal/shared/checkout/rules/_limits.d.ts +14 -1
  72. package/dist/_internal/shared/checkout/rules/_limits.js +14 -1
  73. package/dist/_internal/shared/checkout/rules/_registry.js +2 -0
  74. package/dist/_internal/shared/checkout/rules/auction.rule.js +9 -1
  75. package/dist/_internal/shared/checkout/rules/bundle.rule.js +13 -0
  76. package/dist/_internal/shared/checkout/rules/index.d.ts +2 -0
  77. package/dist/_internal/shared/checkout/rules/index.js +9 -0
  78. package/dist/_internal/shared/checkout/rules/offer.rule.js +0 -1
  79. package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +26 -0
  80. package/dist/_internal/shared/checkout/rules/payment-gate.js +133 -0
  81. package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +29 -8
  82. package/dist/_internal/shared/checkout/rules/spares.rule.d.ts +16 -0
  83. package/dist/_internal/shared/checkout/rules/spares.rule.js +5 -0
  84. package/dist/_internal/shared/checkout/rules/types.d.ts +26 -2
  85. package/dist/_internal/shared/config/schema.d.ts +122 -0
  86. package/dist/_internal/shared/features/cart/schema.js +3 -2
  87. package/dist/_internal/shared/features/products/group-rules.d.ts +75 -0
  88. package/dist/_internal/shared/features/products/group-rules.js +142 -0
  89. package/dist/_internal/shared/listing-types/_registry.d.ts +24 -0
  90. package/dist/_internal/shared/listing-types/_registry.js +39 -1
  91. package/dist/_internal/shared/listing-types/art/config.d.ts +1 -0
  92. package/dist/_internal/shared/listing-types/art/config.js +2 -0
  93. package/dist/_internal/shared/listing-types/auction/config.d.ts +1 -0
  94. package/dist/_internal/shared/listing-types/auction/config.js +2 -0
  95. package/dist/_internal/shared/listing-types/availability.js +48 -0
  96. package/dist/_internal/shared/listing-types/capabilities.js +19 -0
  97. package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -0
  98. package/dist/_internal/shared/listing-types/classified/config.js +2 -0
  99. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -0
  100. package/dist/_internal/shared/listing-types/digital-code/config.js +2 -0
  101. package/dist/_internal/shared/listing-types/feature-flags.js +1 -0
  102. package/dist/_internal/shared/listing-types/live/config.d.ts +1 -0
  103. package/dist/_internal/shared/listing-types/live/config.js +2 -0
  104. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +1 -0
  105. package/dist/_internal/shared/listing-types/pre-order/config.js +2 -0
  106. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -0
  107. package/dist/_internal/shared/listing-types/prize-draw/config.js +2 -0
  108. package/dist/_internal/shared/listing-types/spares/config.d.ts +74 -0
  109. package/dist/_internal/shared/listing-types/spares/config.js +60 -0
  110. package/dist/_internal/shared/listing-types/spares/ctas.d.ts +1 -0
  111. package/dist/_internal/shared/listing-types/spares/ctas.js +4 -0
  112. package/dist/_internal/shared/listing-types/spares/og.js +1 -0
  113. package/dist/_internal/shared/listing-types/spares/schema.d.ts +2 -0
  114. package/dist/_internal/shared/listing-types/spares/schema.js +3 -0
  115. package/dist/_internal/shared/listing-types/spares/seed-factory.d.ts +1 -0
  116. package/dist/_internal/shared/listing-types/spares/seed-factory.js +1 -0
  117. package/dist/_internal/shared/listing-types/standard/config.d.ts +1 -0
  118. package/dist/_internal/shared/listing-types/standard/config.js +2 -0
  119. package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -0
  120. package/dist/_internal/shared/listing-types/stickers/config.js +2 -0
  121. package/dist/_internal/shared/search/match.d.ts +11 -0
  122. package/dist/_internal/shared/search/match.js +30 -3
  123. package/dist/_internal/shared/search/to-groups.d.ts +52 -0
  124. package/dist/_internal/shared/search/to-groups.js +141 -0
  125. package/dist/client.d.ts +8 -2
  126. package/dist/client.js +4 -1
  127. package/dist/constants/api-endpoints.d.ts +46 -15
  128. package/dist/constants/api-endpoints.js +35 -5
  129. package/dist/constants/cache-invalidation.js +3 -1
  130. package/dist/constants/field-names.d.ts +6 -0
  131. package/dist/constants/field-names.js +6 -0
  132. package/dist/features/about/components/PolicyPageView.d.ts +22 -2
  133. package/dist/features/about/components/PolicyPageView.js +14 -2
  134. package/dist/features/about/components/PublicProfileView.js +2 -1
  135. package/dist/features/about/components/ShippingPolicyView.js +42 -2
  136. package/dist/features/account/components/NotificationBell.js +1 -1
  137. package/dist/features/account/hooks/useProfileStats.js +5 -1
  138. package/dist/features/addresses/repository/addresses.repository.d.ts +16 -0
  139. package/dist/features/addresses/repository/addresses.repository.js +21 -0
  140. package/dist/features/admin/actions/admin-actions.js +16 -1
  141. package/dist/features/admin/components/AdminCategoryEditorView.js +19 -2
  142. package/dist/features/admin/components/AdminNavigationView.js +1 -1
  143. package/dist/features/admin/components/AdminPageHeader.js +1 -1
  144. package/dist/features/admin/components/AdminProductEditorView.js +17 -2
  145. package/dist/features/admin/components/AdminSidebar.js +1 -1
  146. package/dist/features/admin/components/AdminSiteSettingsView.js +39 -8
  147. package/dist/features/admin/components/AdminSparesView.d.ts +12 -0
  148. package/dist/features/admin/components/AdminSparesView.js +30 -0
  149. package/dist/features/admin/components/AdminUsersView.js +9 -7
  150. package/dist/features/admin/components/index.d.ts +2 -0
  151. package/dist/features/admin/components/index.js +1 -0
  152. package/dist/features/admin/components/sections/adminSectionsTypes.js +7 -0
  153. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  154. package/dist/features/admin/constants/filter-tabs.js +1 -0
  155. package/dist/features/admin/schemas/firestore.d.ts +26 -2
  156. package/dist/features/auctions/components/AuctionDetailPageView.js +12 -18
  157. package/dist/features/auth/repository/user.repository.d.ts +13 -0
  158. package/dist/features/auth/repository/user.repository.js +28 -0
  159. package/dist/features/auth/schemas/firestore.d.ts +2 -0
  160. package/dist/features/auth/schemas/firestore.js +2 -0
  161. package/dist/features/business-documents/repository/business-documents.repository.d.ts +58 -0
  162. package/dist/features/business-documents/repository/business-documents.repository.js +54 -0
  163. package/dist/features/business-documents/schemas/firestore.d.ts +105 -0
  164. package/dist/features/business-documents/schemas/firestore.js +75 -0
  165. package/dist/features/cart/actions/cart-actions.js +83 -6
  166. package/dist/features/cart/repository/cart.repository.js +32 -4
  167. package/dist/features/cart/schemas/firestore.d.ts +20 -0
  168. package/dist/features/cart/schemas/index.d.ts +2 -2
  169. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +1 -1
  170. package/dist/features/categories/api/[id]/price-guidance/route.d.ts +42 -0
  171. package/dist/features/categories/api/[id]/price-guidance/route.js +90 -0
  172. package/dist/features/categories/components/BrandDetailPageView.js +32 -8
  173. package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
  174. package/dist/features/categories/components/BrandDetailTabs.js +28 -3
  175. package/dist/features/categories/components/BundleCollage.js +3 -2
  176. package/dist/features/categories/components/BundleItemsPicker.js +3 -4
  177. package/dist/features/categories/components/CategoriesIndexListing.js +23 -8
  178. package/dist/features/categories/components/CategoryDetailPageView.js +157 -20
  179. package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
  180. package/dist/features/categories/components/CategoryDetailTabs.js +41 -7
  181. package/dist/features/categories/components/CategoryGrid.js +10 -1
  182. package/dist/features/categories/components/PriceGuidanceHint.d.ts +29 -0
  183. package/dist/features/categories/components/PriceGuidanceHint.js +61 -0
  184. package/dist/features/categories/components/SubcategoryCardRow.d.ts +27 -0
  185. package/dist/features/categories/components/SubcategoryCardRow.js +98 -0
  186. package/dist/features/categories/constants/category-tabs.d.ts +20 -25
  187. package/dist/features/categories/constants/category-tabs.js +22 -39
  188. package/dist/features/categories/repository/categories.repository.d.ts +12 -0
  189. package/dist/features/categories/repository/categories.repository.js +20 -0
  190. package/dist/features/categories/schemas/firestore.d.ts +22 -1
  191. package/dist/features/categories/schemas/firestore.js +10 -0
  192. package/dist/features/categories/schemas/index.d.ts +6 -0
  193. package/dist/features/categories/schemas/index.js +2 -0
  194. package/dist/features/categories/types/index.d.ts +8 -0
  195. package/dist/features/categories/utils/taxonomy-visual.d.ts +16 -3
  196. package/dist/features/categories/utils/taxonomy-visual.js +26 -10
  197. package/dist/features/classified/components/ClassifiedDetailPageView.js +14 -10
  198. package/dist/features/contact/components/ContactPageView.d.ts +33 -1
  199. package/dist/features/contact/components/ContactPageView.js +58 -28
  200. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +14 -10
  201. package/dist/features/grouped/components/GroupedListingDetailView.js +9 -1
  202. package/dist/features/grouped/schemas/firestore.d.ts +5 -0
  203. package/dist/features/homepage/actions/homepage-section-actions.d.ts +3 -3
  204. package/dist/features/homepage/components/CollectionCardsSection.d.ts +28 -6
  205. package/dist/features/homepage/components/CollectionCardsSection.js +43 -8
  206. package/dist/features/homepage/components/HowItWorksSection.d.ts +23 -6
  207. package/dist/features/homepage/components/HowItWorksSection.js +4 -4
  208. package/dist/features/homepage/components/PromoGrid.d.ts +31 -2
  209. package/dist/features/homepage/components/PromoGrid.js +12 -3
  210. package/dist/features/homepage/components/TestimonialsCarousel.d.ts +34 -2
  211. package/dist/features/homepage/components/TestimonialsCarousel.js +1 -1
  212. package/dist/features/homepage/components/index.d.ts +0 -20
  213. package/dist/features/homepage/components/index.js +0 -11
  214. package/dist/features/homepage/index.d.ts +0 -2
  215. package/dist/features/homepage/index.js +0 -2
  216. package/dist/features/homepage/lib/section-renderer.d.ts +9 -0
  217. package/dist/features/homepage/lib/section-renderer.js +202 -58
  218. package/dist/features/homepage/repository/homepage-sections.repository.d.ts +35 -3
  219. package/dist/features/homepage/repository/homepage-sections.repository.js +58 -8
  220. package/dist/features/homepage/schemas/firestore.d.ts +106 -3
  221. package/dist/features/homepage/schemas/firestore.js +32 -2
  222. package/dist/features/homepage/schemas/index.d.ts +36 -7
  223. package/dist/features/homepage/schemas/index.js +39 -27
  224. package/dist/features/homepage/server.d.ts +0 -3
  225. package/dist/features/homepage/server.js +0 -3
  226. package/dist/features/homepage/types/index.d.ts +0 -47
  227. package/dist/features/homepage/types/index.js +49 -0
  228. package/dist/features/layout/AppLayoutShell.js +1 -1
  229. package/dist/features/layout/BottomNavItem.js +1 -1
  230. package/dist/features/layout/BottomSheet.js +1 -1
  231. package/dist/features/layout/TitleBarLayout.js +1 -1
  232. package/dist/features/live/components/LiveItemDetailPageView.js +14 -10
  233. package/dist/features/media/hooks/useMedia.js +17 -0
  234. package/dist/features/media/repository/media-assets.repository.d.ts +46 -0
  235. package/dist/features/media/repository/media-assets.repository.js +31 -0
  236. package/dist/features/media/trust-signals.d.ts +85 -0
  237. package/dist/features/media/trust-signals.js +126 -0
  238. package/dist/features/parts/components/PartDetailPageView.d.ts +14 -1
  239. package/dist/features/parts/components/PartDetailPageView.js +2 -2
  240. package/dist/features/parts/components/PartsIndexPageView.d.ts +11 -2
  241. package/dist/features/parts/components/PartsIndexPageView.js +2 -2
  242. package/dist/features/parts/components/PartsSearchBox.d.ts +9 -0
  243. package/dist/features/parts/components/PartsSearchBox.js +57 -0
  244. package/dist/features/parts/schemas/firestore.d.ts +149 -3
  245. package/dist/features/parts/schemas/firestore.js +184 -0
  246. package/dist/features/payments/repository/payout.repository.d.ts +15 -0
  247. package/dist/features/payments/repository/payout.repository.js +21 -0
  248. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +12 -18
  249. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +1 -1
  250. package/dist/features/price-index/components/PriceIndexToolView.d.ts +24 -1
  251. package/dist/features/price-index/components/PriceIndexToolView.js +299 -17
  252. package/dist/features/price-index/lookup-target.d.ts +48 -0
  253. package/dist/features/price-index/lookup-target.js +107 -0
  254. package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
  255. package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
  256. package/dist/features/products/components/CompareOverlay.d.ts +20 -2
  257. package/dist/features/products/components/CompareOverlay.js +23 -3
  258. package/dist/features/products/components/CustomSectionTabContent.d.ts +31 -0
  259. package/dist/features/products/components/CustomSectionTabContent.js +29 -3
  260. package/dist/features/products/components/GroupMemberPicker.d.ts +19 -1
  261. package/dist/features/products/components/GroupMemberPicker.js +46 -8
  262. package/dist/features/products/components/GroupSettingsPanel.d.ts +11 -1
  263. package/dist/features/products/components/GroupSettingsPanel.js +92 -3
  264. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  265. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +15 -1
  266. package/dist/features/products/components/PrizeDrawEntryActions.js +70 -8
  267. package/dist/features/products/components/ProductDetailPageView.js +81 -17
  268. package/dist/features/products/components/ProductForm.d.ts +13 -1
  269. package/dist/features/products/components/ProductForm.js +16 -2
  270. package/dist/features/products/components/ProductsIndexListing.d.ts +16 -1
  271. package/dist/features/products/components/ProductsIndexListing.js +3 -3
  272. package/dist/features/products/components/ShowGroupSection.js +15 -2
  273. package/dist/features/products/components/SparesListView.d.ts +7 -0
  274. package/dist/features/products/components/SparesListView.js +39 -0
  275. package/dist/features/products/components/SublistingCarouselSection.js +2 -1
  276. package/dist/features/products/constants/action-defs.d.ts +1 -0
  277. package/dist/features/products/constants/action-defs.js +1 -0
  278. package/dist/features/products/repository/products.repository.d.ts +99 -0
  279. package/dist/features/products/repository/products.repository.js +240 -18
  280. package/dist/features/products/schemas/firestore.d.ts +97 -4
  281. package/dist/features/products/schemas/firestore.js +104 -0
  282. package/dist/features/products/schemas/index.d.ts +10 -19
  283. package/dist/features/products/schemas/index.js +23 -6
  284. package/dist/features/products/types/index.d.ts +45 -1
  285. package/dist/features/products/utils/group-member-image.d.ts +46 -0
  286. package/dist/features/products/utils/group-member-image.js +4 -0
  287. package/dist/features/products/utils/listing-badge-variant.js +4 -0
  288. package/dist/features/reviews/components/ReviewsList.js +1 -1
  289. package/dist/features/scams/components/ScamProfileView.js +1 -1
  290. package/dist/features/search/components/EntityTypeahead.d.ts +44 -0
  291. package/dist/features/search/components/EntityTypeahead.js +92 -0
  292. package/dist/features/search/components/Search.d.ts +12 -1
  293. package/dist/features/search/components/Search.js +47 -3
  294. package/dist/features/search/components/SearchFacetRail.d.ts +11 -0
  295. package/dist/features/search/components/SearchFacetRail.js +92 -0
  296. package/dist/features/search/components/SearchResultsClient.d.ts +33 -0
  297. package/dist/features/search/components/SearchResultsClient.js +103 -0
  298. package/dist/features/search/components/SearchResultsPageView.d.ts +15 -1
  299. package/dist/features/search/components/SearchResultsPageView.js +2 -2
  300. package/dist/features/search/components/index.d.ts +0 -6
  301. package/dist/features/search/components/index.js +0 -3
  302. package/dist/features/search/hooks/useNavSuggestions.d.ts +13 -0
  303. package/dist/features/search/hooks/useScopedSearch.d.ts +34 -0
  304. package/dist/features/search/hooks/useScopedSearch.js +114 -0
  305. package/dist/features/search/hooks/useSiteSearch.d.ts +26 -0
  306. package/dist/features/search/hooks/useSiteSearch.js +149 -0
  307. package/dist/features/search/index.d.ts +0 -4
  308. package/dist/features/search/index.js +0 -4
  309. package/dist/features/search/manifest.d.ts +12 -0
  310. package/dist/features/search/manifest.js +17 -2
  311. package/dist/features/search/server.d.ts +0 -3
  312. package/dist/features/search/server.js +0 -3
  313. package/dist/features/seller/components/SellerListingTypeView.d.ts +1 -1
  314. package/dist/features/seller/components/SellerListingTypeView.js +20 -0
  315. package/dist/features/seller/components/SellerPayoutSettingsView.js +35 -14
  316. package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
  317. package/dist/features/seller/components/SellerProductShell.js +2 -1
  318. package/dist/features/seller/components/SellerProductsView.js +3 -2
  319. package/dist/features/seller/components/SellerSidebar.js +1 -1
  320. package/dist/features/seller/components/SellerSparesView.d.ts +17 -0
  321. package/dist/features/seller/components/SellerSparesView.js +6 -0
  322. package/dist/features/seller/components/index.d.ts +2 -0
  323. package/dist/features/seller/components/index.js +1 -0
  324. package/dist/features/shell/CommandPalette.js +1 -1
  325. package/dist/features/sticker-trace/schemas/firestore.d.ts +19 -0
  326. package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
  327. package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
  328. package/dist/features/stores/components/StoreDetailLayoutView.js +7 -82
  329. package/dist/features/stores/components/StoreHeader.js +1 -2
  330. package/dist/features/stores/components/StoreSparesPageView.d.ts +27 -0
  331. package/dist/features/stores/components/StoreSparesPageView.js +48 -0
  332. package/dist/features/stores/schemas/firestore.d.ts +19 -0
  333. package/dist/features/tester/seed-data/authored/buying__spares-and-groups.d.ts +2 -0
  334. package/dist/features/tester/seed-data/authored/buying__spares-and-groups.js +96 -0
  335. package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.d.ts +2 -0
  336. package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +392 -45
  337. package/dist/features/tester/seed-data/authored/content-discovery__category-tabs-franchise-bundles.js +54 -32
  338. package/dist/features/tester/seed-data/authored/content-discovery__parts-database.js +3 -2
  339. package/dist/features/tester/seed-data/authored/content-discovery__price-index-and-compare.d.ts +2 -0
  340. package/dist/features/tester/seed-data/authored/content-discovery__price-index-and-compare.js +73 -0
  341. package/dist/features/tester/seed-data/authored/content-discovery__site-wide-index.d.ts +2 -0
  342. package/dist/features/tester/seed-data/authored/content-discovery__site-wide-index.js +340 -0
  343. package/dist/features/tester/seed-data/authored/index.js +16 -0
  344. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.d.ts +1 -0
  345. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -0
  346. package/dist/features/tester/seed-data/authored/selling__spares-and-group-editing.d.ts +2 -0
  347. package/dist/features/tester/seed-data/authored/selling__spares-and-group-editing.js +77 -0
  348. package/dist/features/tester/seed-data/authored/tools__sticker-promote-to-listings.d.ts +2 -0
  349. package/dist/features/tester/seed-data/authored/tools__sticker-promote-to-listings.js +58 -0
  350. package/dist/features/tester/seed-data/authored/tools__sticker-tracer.js +5 -4
  351. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +384 -18
  352. package/dist/index.d.ts +17 -57
  353. package/dist/index.js +35 -96
  354. package/dist/next/routing/route-map.d.ts +42 -1
  355. package/dist/next/routing/route-map.js +45 -0
  356. package/dist/providers/db-firebase/base.repository.d.ts +22 -0
  357. package/dist/providers/db-firebase/base.repository.js +36 -0
  358. package/dist/schemas/registry.d.ts +15 -6
  359. package/dist/security/blind-index-tokens.d.ts +54 -0
  360. package/dist/security/blind-index-tokens.js +152 -0
  361. package/dist/security/index.d.ts +1 -1
  362. package/dist/security/index.js +1 -1
  363. package/dist/security/settings-encryption.d.ts +35 -0
  364. package/dist/security/settings-encryption.js +91 -0
  365. package/dist/seed/_helpers/brand-rows.js +38 -14
  366. package/dist/seed/_helpers/category-forest.js +73 -7
  367. package/dist/seed/_helpers/category-images-generated.d.ts +25 -6
  368. package/dist/seed/_helpers/category-images-generated.js +81 -6
  369. package/dist/seed/_helpers/category-models.d.ts +1 -1
  370. package/dist/seed/_helpers/category-models.js +7 -1
  371. package/dist/seed/_helpers/category-parts-glossary-map.js +8 -0
  372. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  373. package/dist/seed/_helpers/category-parts.js +50 -2
  374. package/dist/seed/_helpers/character-features.d.ts +1 -1
  375. package/dist/seed/_helpers/character-features.js +2 -1
  376. package/dist/seed/_helpers/franchise-rows.js +10 -2
  377. package/dist/seed/_helpers/media.d.ts +47 -0
  378. package/dist/seed/_helpers/media.js +34 -0
  379. package/dist/seed/authenticity-knowledge-seed-data.d.ts +49 -5
  380. package/dist/seed/authenticity-knowledge-seed-data.js +10001 -549
  381. package/dist/seed/categories-seed-data.js +124 -20
  382. package/dist/seed/grouped-listings-seed-data.js +16 -0
  383. package/dist/seed/homepage-sections-seed-data.js +94 -5
  384. package/dist/seed/index.d.ts +2 -3
  385. package/dist/seed/index.js +2 -2
  386. package/dist/seed/manifest.js +3 -0
  387. package/dist/seed/media-assets-seed-data.d.ts +1 -1
  388. package/dist/seed/media-assets-seed-data.js +746 -196
  389. package/dist/seed/model-composition-seed-data.js +122 -6
  390. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  391. package/dist/seed/part-glossary-seed-data.js +37219 -463
  392. package/dist/seed/products-art-seed-data.js +7 -18
  393. package/dist/seed/products-auctions-seed-data.js +56 -47
  394. package/dist/seed/products-beyblade-x-seed-data.d.ts +2 -0
  395. package/dist/seed/products-beyblade-x-seed-data.js +476 -0
  396. package/dist/seed/products-catalogue-seed-data.d.ts +5 -5
  397. package/dist/seed/products-catalogue-seed-data.js +1500 -1240
  398. package/dist/seed/products-classifieds-seed-data.js +9 -27
  399. package/dist/seed/products-digital-codes-seed-data.js +9 -18
  400. package/dist/seed/products-live-items-seed-data.js +5 -13
  401. package/dist/seed/products-preorders-seed-data.js +67 -33
  402. package/dist/seed/products-prize-draws-seed-data.js +8 -17
  403. package/dist/seed/products-spares-seed-data.d.ts +2 -0
  404. package/dist/seed/products-spares-seed-data.js +368 -0
  405. package/dist/seed/products-standard-seed-data.js +39 -126
  406. package/dist/seed/products-stickers-seed-data.js +59 -15
  407. package/dist/seed/site-settings-seed-data.js +8 -0
  408. package/dist/seo/json-ld.d.ts +25 -0
  409. package/dist/seo/json-ld.js +28 -0
  410. package/dist/server-entry.d.ts +3 -1
  411. package/dist/server-entry.js +2 -2
  412. package/dist/server.d.ts +11 -9
  413. package/dist/server.js +35 -25
  414. package/dist/styles.css +202 -1
  415. package/dist/tailwind-input.css +31 -17
  416. package/dist/tailwind-utilities.css +1 -1
  417. package/dist/ui/components/Accordion.js +1 -1
  418. package/dist/ui/components/Breadcrumb.js +1 -1
  419. package/dist/ui/components/DetailPageGallery.js +0 -2
  420. package/dist/ui/components/DetailPageHero.js +0 -1
  421. package/dist/ui/components/DetailPageTabs.js +1 -1
  422. package/dist/ui/components/Divider.js +3 -3
  423. package/dist/ui/components/GatedPrice.js +5 -1
  424. package/dist/ui/components/HorizontalScroller.js +1 -1
  425. package/dist/ui/components/ListingLayout.js +3 -3
  426. package/dist/ui/components/MediaTabs.style.css +81 -0
  427. package/dist/ui/components/Pagination.js +1 -1
  428. package/dist/ui/components/PendingValue.d.ts +45 -0
  429. package/dist/ui/components/PendingValue.js +7 -0
  430. package/dist/ui/components/Radio.style.css +119 -0
  431. package/dist/ui/components/RatingDisplay.js +2 -2
  432. package/dist/ui/components/SettingsSection.js +0 -2
  433. package/dist/ui/components/SiteLogo.js +3 -0
  434. package/dist/ui/components/SiteMark.js +3 -0
  435. package/dist/ui/components/SortDropdown.js +1 -1
  436. package/dist/ui/components/SpecTable.d.ts +58 -0
  437. package/dist/ui/components/SpecTable.js +8 -0
  438. package/dist/ui/components/StarRating.js +1 -3
  439. package/dist/ui/components/SummaryCard.js +1 -1
  440. package/dist/ui/components/index.style.css +1 -0
  441. package/dist/ui/icons/icon-registry.d.ts +1 -0
  442. package/dist/ui/icons/icon-registry.js +9 -1
  443. package/dist/ui/index.d.ts +4 -0
  444. package/dist/ui/index.js +2 -0
  445. package/dist/utils/search-txt-query.d.ts +12 -0
  446. package/dist/utils/search-txt-query.js +12 -0
  447. package/package.json +1 -1
  448. package/scripts/dedupe-peer-deps.mjs +18 -0
  449. package/scripts/seed-cli.mjs +250 -35
  450. package/scripts/verify-css-build.mjs +118 -37
  451. package/dist/_internal/server/features/search/actions.d.ts +0 -7
  452. package/dist/_internal/server/features/search/actions.js +0 -21
  453. package/dist/_internal/server/features/search/data.d.ts +0 -15
  454. package/dist/_internal/server/features/search/data.js +0 -18
  455. package/dist/_internal/server/features/search/index.d.ts +0 -3
  456. package/dist/_internal/server/features/search/index.js +0 -2
  457. package/dist/features/homepage/api/[id]/route.d.ts +0 -23
  458. package/dist/features/homepage/api/[id]/route.js +0 -103
  459. package/dist/features/homepage/api/route.d.ts +0 -14
  460. package/dist/features/homepage/api/route.js +0 -140
  461. package/dist/features/homepage/api/sections/[id]/route.d.ts +0 -26
  462. package/dist/features/homepage/api/sections/[id]/route.js +0 -95
  463. package/dist/features/homepage/api/sections/route.d.ts +0 -13
  464. package/dist/features/homepage/api/sections/route.js +0 -82
  465. package/dist/features/homepage/components/AdvertisementBanner.d.ts +0 -14
  466. package/dist/features/homepage/components/AdvertisementBanner.js +0 -35
  467. package/dist/features/homepage/components/BeforeAfterCard.d.ts +0 -9
  468. package/dist/features/homepage/components/BeforeAfterCard.js +0 -73
  469. package/dist/features/homepage/components/FeaturedResultsSection.d.ts +0 -17
  470. package/dist/features/homepage/components/FeaturedResultsSection.js +0 -17
  471. package/dist/features/homepage/components/HeroBanner.d.ts +0 -7
  472. package/dist/features/homepage/components/HeroBanner.js +0 -43
  473. package/dist/features/homepage/components/HeroSection.d.ts +0 -7
  474. package/dist/features/homepage/components/HeroSection.js +0 -6
  475. package/dist/features/homepage/components/HomepageSkeleton.d.ts +0 -2
  476. package/dist/features/homepage/components/HomepageSkeleton.js +0 -21
  477. package/dist/features/homepage/components/HomepageView.d.ts +0 -25
  478. package/dist/features/homepage/components/HomepageView.js +0 -5
  479. package/dist/features/homepage/components/NewsletterBanner.d.ts +0 -23
  480. package/dist/features/homepage/components/NewsletterBanner.js +0 -37
  481. package/dist/features/homepage/components/SiteFeaturesSection.d.ts +0 -14
  482. package/dist/features/homepage/components/SiteFeaturesSection.js +0 -11
  483. package/dist/features/homepage/components/TrustBadges.d.ts +0 -6
  484. package/dist/features/homepage/components/TrustBadges.js +0 -46
  485. package/dist/features/homepage/components/TrustIndicatorsSection.d.ts +0 -12
  486. package/dist/features/homepage/components/TrustIndicatorsSection.js +0 -11
  487. package/dist/features/homepage/hooks/useHomepage.d.ts +0 -11
  488. package/dist/features/homepage/hooks/useHomepage.js +0 -17
  489. package/dist/features/homepage/hooks/useHomepageSections.d.ts +0 -2
  490. package/dist/features/homepage/hooks/useHomepageSections.js +0 -10
  491. package/dist/features/homepage/repository/homepage.repository.d.ts +0 -11
  492. package/dist/features/homepage/repository/homepage.repository.js +0 -25
  493. package/dist/features/search/actions/index.d.ts +0 -1
  494. package/dist/features/search/actions/index.js +0 -1
  495. package/dist/features/search/actions/search-actions.d.ts +0 -18
  496. package/dist/features/search/actions/search-actions.js +0 -43
  497. package/dist/features/search/api/route.d.ts +0 -22
  498. package/dist/features/search/api/route.js +0 -128
  499. package/dist/features/search/columns/index.d.ts +0 -17
  500. package/dist/features/search/columns/index.js +0 -49
  501. package/dist/features/search/components/SearchFiltersRow.d.ts +0 -22
  502. package/dist/features/search/components/SearchFiltersRow.js +0 -21
  503. package/dist/features/search/components/SearchResultsSection.d.ts +0 -53
  504. package/dist/features/search/components/SearchResultsSection.js +0 -35
  505. package/dist/features/search/components/SearchView.d.ts +0 -27
  506. package/dist/features/search/components/SearchView.js +0 -8
  507. package/dist/features/search/hooks/useSearch.d.ts +0 -21
  508. package/dist/features/search/hooks/useSearch.js +0 -33
  509. package/dist/features/search/repository/search.repository.d.ts +0 -14
  510. package/dist/features/search/repository/search.repository.js +0 -59
  511. package/dist/features/search/schemas/index.d.ts +0 -70
  512. package/dist/features/search/schemas/index.js +0 -36
  513. package/dist/features/search/types/index.d.ts +0 -48
  514. package/dist/features/stores/components/StoreScopedSearch.d.ts +0 -17
  515. package/dist/features/stores/components/StoreScopedSearch.js +0 -27
  516. package/dist/seed/defaults/homepage-sections.d.ts +0 -2
  517. package/dist/seed/defaults/homepage-sections.js +0 -60
  518. package/dist/seed/factories/homepage-section.factory.d.ts +0 -10
  519. package/dist/seed/factories/homepage-section.factory.js +0 -13
  520. /package/dist/{features/search/types/index.js → _internal/shared/listing-types/spares/og.d.ts} +0 -0
@@ -8,7 +8,7 @@ const __P = {
8
8
  p4: "p-[var(--appkit-space-4)]",
9
9
  p5: "p-[var(--appkit-space-5)]",
10
10
  };
11
- const CLS_BREADCRUMB_LINK = "hover:text-primary-600 transition-colors";
11
+ const CLS_BREADCRUMB_LINK = "hover:text-primary-600 dark:hover:text-primary-400 transition-colors";
12
12
  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";
13
13
  import { ROUTES } from "../../../next/routing/route-map.js";
14
14
  import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
@@ -17,16 +17,17 @@ import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
17
17
  import { safeDisplayName } from "../../../security/index.js";
18
18
  import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal.js";
19
19
  import { isBuyNowAvailable, resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config.js";
20
- import { Button, Container, CountdownDisplay, Div, GatedPrice, PricesOnly, Heading, Input, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li, Dl, Dt, Dd, } from "../../../ui/index.js";
20
+ import { Button, Container, CountdownDisplay, Div, GatedPrice, PricesOnly, Heading, Input, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li, SpecTable, } from "../../../ui/index.js";
21
21
  import { AuctionDetailView } from "../../products/components/AuctionDetailView.js";
22
22
  import { ProductTabsShell } from "../../products/components/ProductTabsShell.js";
23
- import { CustomSectionTabContent } from "../../products/components/CustomSectionTabContent.js";
23
+ import { toCustomSectionTabs } from "../../products/components/CustomSectionTabContent.js";
24
24
  import { AuctionBottomActions } from "./AuctionBottomActions.js";
25
25
  import { AuctionLiveStatusBadge } from "./AuctionLiveStatusBadge.js";
26
26
  import { RelatedProducts } from "../../products/components/RelatedProducts.js";
27
27
  import { ProductGalleryClient } from "../../products/components/ProductGalleryClient.js";
28
28
  import { ProductFeatureBadges } from "../../products/components/ProductFeatureBadges.js";
29
29
  import { FeatureBadgeList } from "../../products/components/FeatureBadge.js";
30
+ import { productImageUrls } from "../../products/schemas/firestore.js";
30
31
  import { HistoryTracker } from "../../history/components/HistoryTracker.js";
31
32
  import { ShareButton } from "../../products/components/ShareButton.js";
32
33
  import { MarketplaceAuctionGrid } from "./MarketplaceAuctionGrid.js";
@@ -149,8 +150,12 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
149
150
  ? p.price
150
151
  : 0;
151
152
  const minBidIncrementOverride = typeof p.minBidIncrement === "number" ? p.minBidIncrement : undefined;
152
- const images = Array.isArray(p.images)
153
- ? p.images
153
+ // `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
154
+ // normaliser (it also accepts the legacy `string[]`). Casting it to
155
+ // `string[]` handed the gallery objects and rendered a placeholder.
156
+ const galleryUrls = productImageUrls(p.images);
157
+ const images = galleryUrls.length > 0
158
+ ? galleryUrls
154
159
  : typeof p.mainImage === "string"
155
160
  ? [p.mainImage]
156
161
  : [];
@@ -184,9 +189,6 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
184
189
  const specs = Array.isArray(p.specifications)
185
190
  ? p.specifications
186
191
  : [];
187
- const customSections = Array.isArray(p.customSections)
188
- ? p.customSections
189
- : [];
190
192
  const descriptionHtml = toDescriptionHtml(p.description);
191
193
  const sublistingCategoryId = typeof p.sublistingCategoryId === "string" ? p.sublistingCategoryId : null;
192
194
  const relatedDocs = (await safeRead(() => productRepository.findByCategory(String(p.category ?? "")), {
@@ -209,11 +211,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
209
211
  });
210
212
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "auction", snapshot: {
211
213
  title: typeof p.title === "string" ? p.title : undefined,
212
- thumb: typeof p.mainImage === "string"
213
- ? p.mainImage
214
- : Array.isArray(p.images) && typeof p.images[0] === "string"
215
- ? p.images[0]
216
- : undefined,
214
+ thumb: typeof p.mainImage === "string" ? p.mainImage : images[0],
217
215
  price: typeof p.price === "number" ? p.price : undefined,
218
216
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
219
217
  storeName: typeof p.storeName === "string" ? p.storeName : undefined,
@@ -230,11 +228,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
230
228
  // prompt here would be the same affordance twice.
231
229
  _jsx(PricesOnly, { children: _jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Buy Now available at ", formatCurrency(buyNowPrice, currency)] }) }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Stack, { className: `lg:hidden ${__P.p4}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "base", badgeSize: "xs" }), endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] })) : !isEnded ? (_jsxs(Div, { className: `${__P.p4} lg:hidden`, border: "subtle", rounded: "xl", surface: "muted", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { size: "base", weight: "bold", className: "text-primary-600 dark:text-primary-400", children: _jsx(GatedPrice, { label: "Sign in to see the current bid", children: formatCurrency(currentBid, currency) }) }), _jsxs(Span, { size: "xs", color: "muted", children: [bidCount, " bids"] })] }), endDate && (_jsxs(Text, { align: "center", className: "mb-3", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderSublistingSection: sublistingCategoryId
232
230
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
233
- : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
234
- id: s.id,
235
- label: s.title,
236
- content: _jsx(CustomSectionTabContent, { section: s }),
237
- })) })), renderBidHistory: () => {
231
+ : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, customTabs: toCustomSectionTabs(p.customSections) })), renderBidHistory: () => {
238
232
  if (!bidsResult)
239
233
  return null;
240
234
  return (_jsx(CollapsibleBidHistory, { productId: String(product.id), initialData: bidsResult, currency: currency }));
@@ -1,3 +1,4 @@
1
+ import type { JsonValue } from "../../../schemas/types";
1
2
  import { BaseRepository, type FirebaseSieveFields, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
2
3
  import type { UserRole } from "../types";
3
4
  import { type UserDocument, type UserPlain } from "../schemas";
@@ -46,6 +47,18 @@ export declare class UserRepository extends BaseRepository<UserDocument> {
46
47
  */
47
48
  protected piiFields: readonly ["email", "phoneNumber", "googleLinkedEmail"];
48
49
  protected piiIndexMap: Record<string, string>;
50
+ /**
51
+ * Admin search by first/last name or email local-part, without decryption.
52
+ * `displayName` and `email` only — `phoneNumber` is excluded on purpose: a
53
+ * phone number has no word boundaries, so tokenising it yields one token
54
+ * that is the whole number, an exact lookup `phoneIndex` already covers.
55
+ *
56
+ * Running through `applyWriteHooks` is what makes a `users` row that
57
+ * SURVIVES a tester cascade (it is PRESERVE-tier and never wiped) keep a
58
+ * correct index going forward, and what makes a NEWLY signed-up real user
59
+ * searchable without anyone having to remember to re-run the backfill.
60
+ */
61
+ protected buildTokensIndexFor(data: Record<string, JsonValue>): Record<string, string[]> | null;
49
62
  /**
50
63
  * The base hook, plus the nested objects it cannot see.
51
64
  *
@@ -1,4 +1,5 @@
1
1
  import { normalizeError } from "../../../errors/normalize.js";
2
+ import { buildTokensIndex } from "../../../security/blind-index-tokens.js";
2
3
  import { DatabaseError } from "../../../errors/index.js";
3
4
  import { BaseRepository, getFirestoreCount, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase/index.js";
4
5
  import { USER_PII_FIELDS, USER_PII_INDEX_MAP, decryptPayoutDetails, maskPii, revealPii, decryptShippingConfig, encryptPayoutDetails, encryptShippingConfig, piiBlindIndex, } from "../../../security/index.js";
@@ -89,6 +90,24 @@ export class UserRepository extends BaseRepository {
89
90
  .filter(([, v]) => typeof v === "string")
90
91
  .map(([k]) => k));
91
92
  }
93
+ /**
94
+ * Admin search by first/last name or email local-part, without decryption.
95
+ * `displayName` and `email` only — `phoneNumber` is excluded on purpose: a
96
+ * phone number has no word boundaries, so tokenising it yields one token
97
+ * that is the whole number, an exact lookup `phoneIndex` already covers.
98
+ *
99
+ * Running through `applyWriteHooks` is what makes a `users` row that
100
+ * SURVIVES a tester cascade (it is PRESERVE-tier and never wiped) keep a
101
+ * correct index going forward, and what makes a NEWLY signed-up real user
102
+ * searchable without anyone having to remember to re-run the backfill.
103
+ */
104
+ buildTokensIndexFor(data) {
105
+ const displayName = typeof data.displayName === "string" ? data.displayName : undefined;
106
+ const email = typeof data.email === "string" ? data.email : undefined;
107
+ if (!displayName && !email)
108
+ return null;
109
+ return { nameTokensIndex: buildTokensIndex(displayName, email) };
110
+ }
92
111
  /**
93
112
  * The base hook, plus the nested objects it cannot see.
94
113
  *
@@ -381,6 +400,15 @@ UserRepository.SIEVE_FIELDS = {
381
400
  // filter matched every employee. The composite indexes it needs are already
382
401
  // declared and deployed — (permissionGroup, role, createdAt|displayName).
383
402
  permissionGroup: { canFilter: true, canSort: false },
403
+ /*
404
+ * Whole-word admin name search. `canSort: false` — an array field has no
405
+ * sort order Firestore can express. Without this entry, `@=` (Firestore
406
+ * `array-contains`) on `nameTokensIndex` is dropped SILENTLY by Sieve
407
+ * (`throwExceptions: false`), and the clause after it goes with it —
408
+ * which here would be nothing, but is the exact mechanism Root Cause #62
409
+ * documents for every other field this has happened to.
410
+ */
411
+ nameTokensIndex: { canFilter: true, canSort: false },
384
412
  createdAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
385
413
  updatedAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
386
414
  };
@@ -271,6 +271,8 @@ export declare const USER_FIELDS: {
271
271
  readonly DISPLAY_NAME: "displayName";
272
272
  readonly EMAIL_INDEX: "emailIndex";
273
273
  readonly PHONE_INDEX: "phoneIndex";
274
+ /** Whole-word HMAC digests of `displayName` + `email`. See `blind-index-tokens.ts`. */
275
+ readonly NAME_TOKENS_INDEX: "nameTokensIndex";
274
276
  readonly PHOTO_URL: "photoURL";
275
277
  readonly AVATAR_METADATA: "avatarMetadata";
276
278
  readonly ROLE: "role";
@@ -102,6 +102,8 @@ export const USER_FIELDS = {
102
102
  DISPLAY_NAME: "displayName",
103
103
  EMAIL_INDEX: "emailIndex",
104
104
  PHONE_INDEX: "phoneIndex",
105
+ /** Whole-word HMAC digests of `displayName` + `email`. See `blind-index-tokens.ts`. */
106
+ NAME_TOKENS_INDEX: "nameTokensIndex",
105
107
  PHOTO_URL: "photoURL",
106
108
  AVATAR_METADATA: "avatarMetadata",
107
109
  ROLE: "role",
@@ -0,0 +1,58 @@
1
+ /**
2
+ * BusinessDocumentsRepository — thin wrapper over the `businessDocuments`
3
+ * collection. See `../schemas/firestore.ts` for the doc shape and for why the
4
+ * bytes are not in Firestore.
5
+ */
6
+ import { BaseRepository, parseSieveDateValue, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
7
+ import { type BusinessDocumentDocument, type BusinessDocumentKind } from "../schemas/firestore";
8
+ export declare class BusinessDocumentsRepository extends BaseRepository<BusinessDocumentDocument> {
9
+ constructor();
10
+ static readonly SIEVE_FIELDS: {
11
+ id: {
12
+ canFilter: boolean;
13
+ canSort: boolean;
14
+ };
15
+ kind: {
16
+ canFilter: boolean;
17
+ canSort: boolean;
18
+ };
19
+ fileName: {
20
+ canFilter: boolean;
21
+ canSort: boolean;
22
+ };
23
+ uploadedBy: {
24
+ canFilter: boolean;
25
+ canSort: boolean;
26
+ };
27
+ uploadedAt: {
28
+ canFilter: boolean;
29
+ canSort: boolean;
30
+ parseValue: typeof parseSieveDateValue;
31
+ };
32
+ createdAt: {
33
+ canFilter: boolean;
34
+ canSort: boolean;
35
+ parseValue: typeof parseSieveDateValue;
36
+ };
37
+ };
38
+ list(model: SieveModel): Promise<FirebaseSieveResult<BusinessDocumentDocument>>;
39
+ /**
40
+ * The current document of a given kind — newest `uploadedAt` wins.
41
+ *
42
+ * Re-uploading a re-issued certificate is normal and both copies are kept:
43
+ * the superseded one is the evidence of what was true at the time, which is
44
+ * the entire point of keeping a registration document. So this resolves the
45
+ * current one rather than assuming a single row per kind.
46
+ *
47
+ * 🛑 Ordered by `createdAt`, NOT `uploadedAt`, even though the latter reads
48
+ * more naturally here. `BaseRepository.create()` stamps `createdAt` on every
49
+ * document it writes, so it is guaranteed present; `uploadedAt` is set by the
50
+ * caller and a single write path that forgot it would make that row vanish
51
+ * from this query — a Firestore `orderBy` on a field a document lacks is a
52
+ * silent `WHERE field IS NOT NULL` (Root Cause #100). The two are written in
53
+ * the same operation and agree, so there is nothing to gain by taking the
54
+ * field that can be missed.
55
+ */
56
+ findCurrentByKind(kind: BusinessDocumentKind): Promise<BusinessDocumentDocument | null>;
57
+ }
58
+ export declare const businessDocumentsRepository: BusinessDocumentsRepository;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * BusinessDocumentsRepository — thin wrapper over the `businessDocuments`
3
+ * collection. See `../schemas/firestore.ts` for the doc shape and for why the
4
+ * bytes are not in Firestore.
5
+ */
6
+ import { BaseRepository, parseSieveDateValue, } from "../../../providers/db-firebase/index.js";
7
+ import { BUSINESS_DOCUMENTS_COLLECTION, } from "../schemas/firestore.js";
8
+ export class BusinessDocumentsRepository extends BaseRepository {
9
+ constructor() {
10
+ super(BUSINESS_DOCUMENTS_COLLECTION);
11
+ }
12
+ async list(model) {
13
+ return this.sieveQuery(model, BusinessDocumentsRepository.SIEVE_FIELDS, { defaultPageSize: 25, maxPageSize: 100 });
14
+ }
15
+ /**
16
+ * The current document of a given kind — newest `uploadedAt` wins.
17
+ *
18
+ * Re-uploading a re-issued certificate is normal and both copies are kept:
19
+ * the superseded one is the evidence of what was true at the time, which is
20
+ * the entire point of keeping a registration document. So this resolves the
21
+ * current one rather than assuming a single row per kind.
22
+ *
23
+ * 🛑 Ordered by `createdAt`, NOT `uploadedAt`, even though the latter reads
24
+ * more naturally here. `BaseRepository.create()` stamps `createdAt` on every
25
+ * document it writes, so it is guaranteed present; `uploadedAt` is set by the
26
+ * caller and a single write path that forgot it would make that row vanish
27
+ * from this query — a Firestore `orderBy` on a field a document lacks is a
28
+ * silent `WHERE field IS NOT NULL` (Root Cause #100). The two are written in
29
+ * the same operation and agree, so there is nothing to gain by taking the
30
+ * field that can be missed.
31
+ */
32
+ async findCurrentByKind(kind) {
33
+ const result = await this.list({
34
+ filters: `kind==${kind}`,
35
+ sorts: "-createdAt",
36
+ page: 1,
37
+ pageSize: 1,
38
+ });
39
+ return result.items[0] ?? null;
40
+ }
41
+ }
42
+ BusinessDocumentsRepository.SIEVE_FIELDS = {
43
+ id: { canFilter: true, canSort: false },
44
+ kind: { canFilter: true, canSort: true },
45
+ fileName: { canFilter: true, canSort: true },
46
+ uploadedBy: { canFilter: true, canSort: false },
47
+ // Timestamp fields that are filterable MUST carry parseValue, or an ISO
48
+ // string reaches Firestore as a string, the type does not match the stored
49
+ // Timestamp, and the query matches ZERO documents with no error anywhere
50
+ // (Root Cause #47).
51
+ uploadedAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
52
+ createdAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
53
+ };
54
+ export const businessDocumentsRepository = new BusinessDocumentsRepository();
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Business Documents Firestore Document Types & Constants
3
+ *
4
+ * Collection: businessDocuments
5
+ * Document ID: Firestore auto-ID (no slug prefix — same convention as
6
+ * `adminAuditLog` / `notifications` / `sessions` / `jobs`; see CLAUDE.md's
7
+ * "True Firestore auto-IDs" list).
8
+ *
9
+ * The statutory registration documents of the business itself — GST
10
+ * certificate, Udyam registration, bank passbook. These are NOT product media
11
+ * and NOT user uploads; there is exactly one current document per `kind`.
12
+ *
13
+ * ## 🛑 Metadata only. The bytes are never in Firestore.
14
+ *
15
+ * A Firestore document is capped at 1 MiB and the Udyam certificate alone is
16
+ * 680 KB, which base64-encodes to ~907 KB — so an "encrypt the PDF into a
17
+ * string field" design is one slightly larger certificate away from an
18
+ * unrecoverable write failure, at upload time, after the user has handed over
19
+ * the document. The ciphertext lives in Storage; this collection records where.
20
+ *
21
+ * ## 🛑 Why `storagePath` must never be served through /api/media
22
+ *
23
+ * `GET /api/media/[...slug]` applies NO authentication — deliberately, because
24
+ * its job is serving public product photography through a watermarker — and
25
+ * `storage.rules` is `allow read: if true` for every object in the bucket. So
26
+ * the only thing standing between these documents and the public internet is
27
+ * that nothing routes them there, and that they carry no `mediaAssets` row (the
28
+ * row IS the public `/media/<shortId>` slug, so no row means no public URL).
29
+ *
30
+ * Read them through `GET /api/admin/business-documents/[id]` and nothing else.
31
+ */
32
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
33
+ export declare const BUSINESS_DOCUMENTS_COLLECTION = "businessDocuments";
34
+ export declare const BusinessDocumentKindValues: {
35
+ readonly GST_CERTIFICATE: "gst_certificate";
36
+ readonly UDYAM_CERTIFICATE: "udyam_certificate";
37
+ readonly BANK_PASSBOOK: "bank_passbook";
38
+ };
39
+ export type BusinessDocumentKind = (typeof BusinessDocumentKindValues)[keyof typeof BusinessDocumentKindValues];
40
+ export declare const ALL_BUSINESS_DOCUMENT_KINDS: readonly BusinessDocumentKind[];
41
+ /**
42
+ * A `Record<BusinessDocumentKind, string>` rather than a lookup with a
43
+ * fallback, so adding a fourth kind is a COMPILE error here instead of
44
+ * rendering as a raw snake_case id in the admin list.
45
+ */
46
+ export declare const BUSINESS_DOCUMENT_KIND_LABELS: Record<BusinessDocumentKind, string>;
47
+ export interface BusinessDocumentDocument extends BaseDocument {
48
+ kind: BusinessDocumentKind;
49
+ /**
50
+ * Path inside the Storage bucket, e.g.
51
+ * `private/business-documents/3f2a…-gst-certificate.pdf`.
52
+ *
53
+ * 🛑 Never returned by any API, to anyone. The download route resolves it
54
+ * server-side and streams the decrypted bytes; handing the path to a client
55
+ * would let it be fetched straight from Storage, where the rules permit any
56
+ * read.
57
+ */
58
+ storagePath: string;
59
+ /** Original filename as uploaded — shown in the admin list and used for Content-Disposition. */
60
+ fileName: string;
61
+ /** Verified against the file's magic bytes at upload; never trusted from the client. */
62
+ contentType: string;
63
+ /** Size of the PLAINTEXT, before the envelope. What an admin expects to see downloaded. */
64
+ sizeBytes: number;
65
+ /**
66
+ * Whether the stored object carries the `enc:v1:` binary envelope.
67
+ *
68
+ * Always `true` for anything this codebase writes — `encryptBuffer` throws
69
+ * rather than degrading when `SETTINGS_ENCRYPTION_KEY` is unset, so there is
70
+ * no path that produces a plaintext object. It is recorded anyway because
71
+ * the download route must not GUESS: a future hand-uploaded object would
72
+ * otherwise be fed to `decryptBuffer` and throw an envelope error that
73
+ * describes the crypto rather than the upload.
74
+ */
75
+ encrypted: boolean;
76
+ /** Auth UID of the admin who uploaded it. */
77
+ uploadedBy: string;
78
+ uploadedAt: Date;
79
+ }
80
+ export declare const BUSINESS_DOCUMENT_FIELDS: {
81
+ readonly KIND: "kind";
82
+ readonly STORAGE_PATH: "storagePath";
83
+ readonly FILE_NAME: "fileName";
84
+ readonly CONTENT_TYPE: "contentType";
85
+ readonly SIZE_BYTES: "sizeBytes";
86
+ readonly ENCRYPTED: "encrypted";
87
+ readonly UPLOADED_BY: "uploadedBy";
88
+ readonly UPLOADED_AT: "uploadedAt";
89
+ readonly CREATED_AT: "createdAt";
90
+ readonly KIND_VALUES: {
91
+ readonly GST_CERTIFICATE: "gst_certificate";
92
+ readonly UDYAM_CERTIFICATE: "udyam_certificate";
93
+ readonly BANK_PASSBOOK: "bank_passbook";
94
+ };
95
+ };
96
+ /** Storage prefix. Mirrors `private/digital-content/` — outside any public read path. */
97
+ export declare const BUSINESS_DOCUMENTS_STORAGE_PREFIX = "private/business-documents";
98
+ /**
99
+ * Accepted upload types. PDFs and scans only.
100
+ *
101
+ * Checked against the file's MAGIC BYTES, not the declared Content-Type: a
102
+ * declared type is a claim by the uploader, and an HTML document served from
103
+ * this origin would be stored XSS against a signed-in admin.
104
+ */
105
+ export declare const BUSINESS_DOCUMENT_ALLOWED_TYPES: readonly ["application/pdf", "image/jpeg", "image/png", "image/webp"];
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Business Documents Firestore Document Types & Constants
3
+ *
4
+ * Collection: businessDocuments
5
+ * Document ID: Firestore auto-ID (no slug prefix — same convention as
6
+ * `adminAuditLog` / `notifications` / `sessions` / `jobs`; see CLAUDE.md's
7
+ * "True Firestore auto-IDs" list).
8
+ *
9
+ * The statutory registration documents of the business itself — GST
10
+ * certificate, Udyam registration, bank passbook. These are NOT product media
11
+ * and NOT user uploads; there is exactly one current document per `kind`.
12
+ *
13
+ * ## 🛑 Metadata only. The bytes are never in Firestore.
14
+ *
15
+ * A Firestore document is capped at 1 MiB and the Udyam certificate alone is
16
+ * 680 KB, which base64-encodes to ~907 KB — so an "encrypt the PDF into a
17
+ * string field" design is one slightly larger certificate away from an
18
+ * unrecoverable write failure, at upload time, after the user has handed over
19
+ * the document. The ciphertext lives in Storage; this collection records where.
20
+ *
21
+ * ## 🛑 Why `storagePath` must never be served through /api/media
22
+ *
23
+ * `GET /api/media/[...slug]` applies NO authentication — deliberately, because
24
+ * its job is serving public product photography through a watermarker — and
25
+ * `storage.rules` is `allow read: if true` for every object in the bucket. So
26
+ * the only thing standing between these documents and the public internet is
27
+ * that nothing routes them there, and that they carry no `mediaAssets` row (the
28
+ * row IS the public `/media/<shortId>` slug, so no row means no public URL).
29
+ *
30
+ * Read them through `GET /api/admin/business-documents/[id]` and nothing else.
31
+ */
32
+ export const BUSINESS_DOCUMENTS_COLLECTION = "businessDocuments";
33
+ export const BusinessDocumentKindValues = {
34
+ GST_CERTIFICATE: "gst_certificate",
35
+ UDYAM_CERTIFICATE: "udyam_certificate",
36
+ BANK_PASSBOOK: "bank_passbook",
37
+ };
38
+ export const ALL_BUSINESS_DOCUMENT_KINDS = Object.values(BusinessDocumentKindValues);
39
+ /**
40
+ * A `Record<BusinessDocumentKind, string>` rather than a lookup with a
41
+ * fallback, so adding a fourth kind is a COMPILE error here instead of
42
+ * rendering as a raw snake_case id in the admin list.
43
+ */
44
+ export const BUSINESS_DOCUMENT_KIND_LABELS = {
45
+ [BusinessDocumentKindValues.GST_CERTIFICATE]: "GST Certificate",
46
+ [BusinessDocumentKindValues.UDYAM_CERTIFICATE]: "Udyam Registration Certificate",
47
+ [BusinessDocumentKindValues.BANK_PASSBOOK]: "Bank Passbook",
48
+ };
49
+ export const BUSINESS_DOCUMENT_FIELDS = {
50
+ KIND: "kind",
51
+ STORAGE_PATH: "storagePath",
52
+ FILE_NAME: "fileName",
53
+ CONTENT_TYPE: "contentType",
54
+ SIZE_BYTES: "sizeBytes",
55
+ ENCRYPTED: "encrypted",
56
+ UPLOADED_BY: "uploadedBy",
57
+ UPLOADED_AT: "uploadedAt",
58
+ CREATED_AT: "createdAt",
59
+ KIND_VALUES: BusinessDocumentKindValues,
60
+ };
61
+ /** Storage prefix. Mirrors `private/digital-content/` — outside any public read path. */
62
+ export const BUSINESS_DOCUMENTS_STORAGE_PREFIX = "private/business-documents";
63
+ /**
64
+ * Accepted upload types. PDFs and scans only.
65
+ *
66
+ * Checked against the file's MAGIC BYTES, not the declared Content-Type: a
67
+ * declared type is a claim by the uploader, and an HTML document served from
68
+ * this origin would be stored XSS against a signed-in admin.
69
+ */
70
+ export const BUSINESS_DOCUMENT_ALLOWED_TYPES = [
71
+ "application/pdf",
72
+ "image/jpeg",
73
+ "image/png",
74
+ "image/webp",
75
+ ];
@@ -15,6 +15,7 @@ import { CART_LANE, activeLane, canAddNewItems, } from "../../../_internal/share
15
15
  import { CART_MAX_ITEMS } from "../../../constants/limits.js";
16
16
  import { getExpandedDecrements, validateCartItemStock, } from "../../../_internal/server/features/checkout/bundle-expansion.js";
17
17
  import { safeRead } from "../../../errors/safe-read.js";
18
+ import { checkGroupRules, readGroupRules, } from "../../../_internal/shared/features/products/group-rules.js";
18
19
  /**
19
20
  * Product documents don't always carry a denormalized `storeName` — the
20
21
  * create routes only set it when the caller explicitly passes one, so
@@ -133,7 +134,13 @@ export async function addGroupLineToCart(userId, input) {
133
134
  if (!rule.cartEligible) {
134
135
  throw new ValidationError(`"${product.title}" can't be added to the cart. ${rule.cartIneligibleHint ?? ""}`.trim());
135
136
  }
136
- if ((product.availableQuantity ?? 0) < m.quantity) {
137
+ /*
138
+ * 🛑 Made to order: no stock CHECK for this member. Nothing is held in
139
+ * reserve — it is printed or built when the order arrives — so the only
140
+ * honest answer to "how many are left" is "as many as you want". The
141
+ * decrement still happens at checkout; see `validateCartItemStock`.
142
+ */
143
+ if (product.madeToOrder !== true && (product.availableQuantity ?? 0) < m.quantity) {
137
144
  throw new ValidationError(`Only ${product.availableQuantity ?? 0} left of "${product.title}".`);
138
145
  }
139
146
  resolved.push({
@@ -141,7 +148,7 @@ export async function addGroupLineToCart(userId, input) {
141
148
  quantity: m.quantity,
142
149
  unitPrice: product.price,
143
150
  title: product.title,
144
- image: product.images?.[0] ?? product.mainImage,
151
+ image: product.images?.[0]?.url ?? product.mainImage,
145
152
  gstRate: product.gstRate,
146
153
  hsnCode: product.hsnCode,
147
154
  listingType,
@@ -164,7 +171,7 @@ export async function addGroupLineToCart(userId, input) {
164
171
  return addItemToCart(userId, {
165
172
  productId: first.id,
166
173
  productTitle: first.title,
167
- productImage: first.images?.[0] ?? first.mainImage,
174
+ productImage: first.images?.[0]?.url ?? first.mainImage,
168
175
  price: first.price,
169
176
  currency: first.currency,
170
177
  quantity: resolved[0].quantity,
@@ -192,6 +199,25 @@ export async function addGroupLineToCart(userId, input) {
192
199
  throw new ValidationError(`Only ${shortfall.availableQty} left of "${short?.title ?? shortfall.productId}" once your cart is counted.`);
193
200
  }
194
201
  const groupTitle = await resolveGroupTitle(groupId, groupSource, first ?? null);
202
+ /*
203
+ * 🛑 SERVER-SIDE ENFORCEMENT OF THE GROUP'S OWN RULES.
204
+ *
205
+ * `GroupMemberPicker` mirrors this so a buyer is not told "no" only after
206
+ * submitting — but the picker is a convenience and this is the boundary. The
207
+ * two share `checkGroupRules`, so they cannot disagree about what a valid
208
+ * selection is; only about WHEN the buyer finds out.
209
+ *
210
+ * It throws on the FIRST violation while the picker shows all of them
211
+ * together, which is each surface doing the right thing for its own job: a
212
+ * form lists everything to fix, an API answers one reason.
213
+ */
214
+ const rules = await resolveGroupRules(groupId, groupSource, first ?? null);
215
+ const violations = checkGroupRules(rules, resolved.map((r) => ({ productId: r.productId, quantity: r.quantity, title: r.title })));
216
+ if (violations.length > 0) {
217
+ throw Object.assign(new ValidationError(violations[0].message), {
218
+ code: violations[0].code,
219
+ });
220
+ }
195
221
  return cartRepository.addGroupLine(userId, {
196
222
  productId: groupId,
197
223
  productTitle: groupTitle,
@@ -208,9 +234,51 @@ export async function addGroupLineToCart(userId, input) {
208
234
  groupId,
209
235
  groupSlug: groupSource === "product-group" ? groupId : undefined,
210
236
  groupTitle,
237
+ /*
238
+ * Snapshotted at add time — see `CartItemDocument.groupDiscountPercent`.
239
+ * Omitted entirely when there is no discount, so a line with no group
240
+ * pricing carries no field rather than a zero that reads as "0% off".
241
+ */
242
+ ...(rules.groupDiscountPercent !== undefined && rules.groupDiscountPercent > 0
243
+ ? { groupDiscountPercent: rules.groupDiscountPercent }
244
+ : {}),
211
245
  groupMembers: resolved,
212
246
  });
213
247
  }
248
+ /**
249
+ * The group's selection rules, from whichever of the two homes owns them.
250
+ *
251
+ * 🛑 ONE resolver, because there are TWO homes: a product group keeps them on
252
+ * its PARENT PRODUCT (identified by the parent's slug, which IS the groupId),
253
+ * while a `groupedListing` keeps them on its own document. Reading them at each
254
+ * call site is how the picker and the server come to disagree.
255
+ *
256
+ * Fails OPEN — an unreadable group yields no rules rather than refusing the
257
+ * add. A transient Firestore error must not look like "your selection is
258
+ * invalid", which is a statement about the buyer's choice and would send them
259
+ * to change something that was already correct.
260
+ */
261
+ async function resolveGroupRules(groupId, groupSource, sample) {
262
+ if (groupSource === "grouped-listing") {
263
+ const { groupedListingsRepository } = await import("../../grouped/repository/grouped-listings.repository.js");
264
+ const group = await safeRead(() => groupedListingsRepository.findById(groupId), {
265
+ route: "/cart", key: "groupedListings.rules", fallback: null,
266
+ });
267
+ return readGroupRules(group);
268
+ }
269
+ /*
270
+ * A product group's rules live on the PARENT, and `sample` is an arbitrary
271
+ * member — so the parent has to be fetched unless the sample happens to BE
272
+ * it. `groupId` is the parent's slug and products are pure slugs
273
+ * (`id === slug`), so `findById` resolves it directly.
274
+ */
275
+ if (sample?.isGroupParent && sample.id === groupId)
276
+ return readGroupRules(sample);
277
+ const parent = await safeRead(() => productRepository.findById(groupId), {
278
+ route: "/cart", key: "products.groupParentRules", fallback: null,
279
+ });
280
+ return readGroupRules(parent);
281
+ }
214
282
  /** Is this product listed on that `groupedListings` document? */
215
283
  async function isGroupedListingMember(groupId, productId) {
216
284
  const { groupedListingsRepository } = await import("../../grouped/repository/grouped-listings.repository.js");
@@ -263,14 +331,23 @@ export async function mergeGuestCart(userId, items) {
263
331
  const product = await productRepository.findById(item.productId);
264
332
  if (!product || product.status !== "published")
265
333
  continue;
266
- if (product.availableQuantity < 1)
334
+ /*
335
+ * A made-to-order row survives the guest-cart merge at its full requested
336
+ * quantity. Dropping it (or clamping it to 0) would silently delete the
337
+ * line a signed-out visitor had chosen, which reads as the cart losing
338
+ * items on login — and there is no stock level for the clamp to clamp to.
339
+ */
340
+ const unlimited = product.madeToOrder === true;
341
+ if (!unlimited && product.availableQuantity < 1)
267
342
  continue;
268
- const safeQty = Math.min(item.quantity, product.availableQuantity);
343
+ const safeQty = unlimited
344
+ ? item.quantity
345
+ : Math.min(item.quantity, product.availableQuantity);
269
346
  const storeName = await resolveStoreName(product.storeId, product.storeName);
270
347
  await cartRepository.addItem(userId, {
271
348
  productId: product.id,
272
349
  productTitle: product.title,
273
- productImage: product.images?.[0] ?? "",
350
+ productImage: product.images?.[0]?.url ?? "",
274
351
  price: product.price,
275
352
  currency: product.currency ?? getDefaultCurrency(),
276
353
  quantity: safeQty,