@mohasinac/appkit 2.6.0 → 2.6.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 (304) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +38 -0
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +75 -0
  3. package/dist/_internal/client/features/layout/RoleGuard.d.ts +15 -0
  4. package/dist/_internal/client/features/layout/RoleGuard.js +25 -0
  5. package/dist/_internal/client/features/layout/index.d.ts +5 -0
  6. package/dist/_internal/client/features/layout/index.js +4 -0
  7. package/dist/_internal/server/features/brands/actions.d.ts +3 -3
  8. package/dist/_internal/server/features/brands/actions.js +72 -5
  9. package/dist/_internal/server/features/brands/data.d.ts +8 -8
  10. package/dist/_internal/server/features/brands/data.js +10 -11
  11. package/dist/_internal/server/features/brands/service.d.ts +2 -2
  12. package/dist/_internal/server/features/brands/service.js +5 -5
  13. package/dist/_internal/server/features/categories/og.d.ts +33 -0
  14. package/dist/_internal/server/features/categories/og.js +75 -0
  15. package/dist/_internal/server/features/checkout/actions.d.ts +24 -0
  16. package/dist/_internal/server/features/checkout/actions.js +442 -13
  17. package/dist/_internal/server/features/checkout/index.d.ts +1 -1
  18. package/dist/_internal/server/features/checkout/index.js +1 -1
  19. package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +59 -0
  20. package/dist/_internal/server/features/checkout/prize-bundle-gates.js +99 -0
  21. package/dist/_internal/server/features/grouped/data.js +12 -5
  22. package/dist/_internal/server/features/homepage/data.d.ts +1 -1
  23. package/dist/_internal/server/features/homepage/data.js +2 -2
  24. package/dist/_internal/server/features/media/contextGuards.d.ts +52 -0
  25. package/dist/_internal/server/features/media/contextGuards.js +198 -0
  26. package/dist/_internal/server/features/orders/adapters.js +12 -0
  27. package/dist/_internal/server/features/products/data.d.ts +1 -1
  28. package/dist/_internal/server/features/sublisting-categories/data.d.ts +1 -1
  29. package/dist/_internal/server/features/sublisting-categories/data.js +2 -2
  30. package/dist/_internal/server/jobs/handlers/assignSpinPrize.d.ts +24 -0
  31. package/dist/_internal/server/jobs/handlers/assignSpinPrize.js +86 -0
  32. package/dist/_internal/server/jobs/handlers/bundleStockSync.d.ts +18 -0
  33. package/dist/_internal/server/jobs/handlers/bundleStockSync.js +80 -0
  34. package/dist/_internal/server/jobs/handlers/index.d.ts +8 -0
  35. package/dist/_internal/server/jobs/handlers/index.js +13 -0
  36. package/dist/_internal/server/jobs/handlers/listingProcessor.js +13 -3
  37. package/dist/_internal/server/jobs/handlers/onProductStockChange.d.ts +17 -0
  38. package/dist/_internal/server/jobs/handlers/onProductStockChange.js +136 -0
  39. package/dist/_internal/server/jobs/handlers/onProductWrite.js +17 -1
  40. package/dist/_internal/server/jobs/handlers/prizeRevealClose.d.ts +9 -0
  41. package/dist/_internal/server/jobs/handlers/prizeRevealClose.js +29 -0
  42. package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.d.ts +10 -0
  43. package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.js +58 -0
  44. package/dist/_internal/server/jobs/handlers/prizeRevealOpen.d.ts +10 -0
  45. package/dist/_internal/server/jobs/handlers/prizeRevealOpen.js +65 -0
  46. package/dist/_internal/server/jobs/handlers/prizeRevealReminder.d.ts +9 -0
  47. package/dist/_internal/server/jobs/handlers/prizeRevealReminder.js +45 -0
  48. package/dist/_internal/server/jobs/handlers/triggerEventRaffle.d.ts +30 -0
  49. package/dist/_internal/server/jobs/handlers/triggerEventRaffle.js +94 -0
  50. package/dist/_internal/shared/features/brands/schema.d.ts +3 -3
  51. package/dist/_internal/shared/features/cart/schema.d.ts +8 -8
  52. package/dist/_internal/shared/features/cart/schema.js +1 -1
  53. package/dist/_internal/shared/features/categories/bundle-config.d.ts +17 -0
  54. package/dist/_internal/shared/features/categories/bundle-config.js +17 -0
  55. package/dist/_internal/shared/features/layout/config.d.ts +35 -0
  56. package/dist/_internal/shared/features/layout/config.js +58 -0
  57. package/dist/_internal/shared/features/layout/index.d.ts +3 -0
  58. package/dist/_internal/shared/features/layout/index.js +2 -0
  59. package/dist/_internal/shared/features/layout/types.d.ts +137 -0
  60. package/dist/_internal/shared/features/layout/types.js +13 -0
  61. package/dist/_internal/shared/features/products/types.d.ts +1 -1
  62. package/dist/_internal/shared/listing-types/_registry.d.ts +57 -0
  63. package/dist/_internal/shared/listing-types/_registry.js +28 -0
  64. package/dist/_internal/shared/listing-types/auction/config.d.ts +7 -0
  65. package/dist/_internal/shared/listing-types/auction/config.js +8 -0
  66. package/dist/_internal/shared/listing-types/auction/ctas.d.ts +1 -0
  67. package/dist/_internal/shared/listing-types/auction/ctas.js +2 -0
  68. package/dist/_internal/shared/listing-types/auction/og.d.ts +1 -0
  69. package/dist/_internal/shared/listing-types/auction/og.js +1 -0
  70. package/dist/_internal/shared/listing-types/auction/schema.d.ts +1 -0
  71. package/dist/_internal/shared/listing-types/auction/schema.js +1 -0
  72. package/dist/_internal/shared/listing-types/auction/seed-factory.d.ts +1 -0
  73. package/dist/_internal/shared/listing-types/auction/seed-factory.js +1 -0
  74. package/dist/_internal/shared/listing-types/capabilities.d.ts +41 -0
  75. package/dist/_internal/shared/listing-types/capabilities.js +75 -0
  76. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +7 -0
  77. package/dist/_internal/shared/listing-types/pre-order/config.js +8 -0
  78. package/dist/_internal/shared/listing-types/pre-order/ctas.d.ts +1 -0
  79. package/dist/_internal/shared/listing-types/pre-order/ctas.js +2 -0
  80. package/dist/_internal/shared/listing-types/pre-order/og.d.ts +1 -0
  81. package/dist/_internal/shared/listing-types/pre-order/og.js +1 -0
  82. package/dist/_internal/shared/listing-types/pre-order/schema.d.ts +1 -0
  83. package/dist/_internal/shared/listing-types/pre-order/schema.js +1 -0
  84. package/dist/_internal/shared/listing-types/pre-order/seed-factory.d.ts +1 -0
  85. package/dist/_internal/shared/listing-types/pre-order/seed-factory.js +1 -0
  86. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +7 -0
  87. package/dist/_internal/shared/listing-types/prize-draw/config.js +8 -0
  88. package/dist/_internal/shared/listing-types/prize-draw/ctas.d.ts +1 -0
  89. package/dist/_internal/shared/listing-types/prize-draw/ctas.js +2 -0
  90. package/dist/_internal/shared/listing-types/prize-draw/og.d.ts +1 -0
  91. package/dist/_internal/shared/listing-types/prize-draw/og.js +1 -0
  92. package/dist/_internal/shared/listing-types/prize-draw/schema.d.ts +1 -0
  93. package/dist/_internal/shared/listing-types/prize-draw/schema.js +1 -0
  94. package/dist/_internal/shared/listing-types/prize-draw/seed-factory.d.ts +1 -0
  95. package/dist/_internal/shared/listing-types/prize-draw/seed-factory.js +1 -0
  96. package/dist/_internal/shared/listing-types/standard/config.d.ts +7 -0
  97. package/dist/_internal/shared/listing-types/standard/config.js +8 -0
  98. package/dist/_internal/shared/listing-types/standard/ctas.d.ts +1 -0
  99. package/dist/_internal/shared/listing-types/standard/ctas.js +3 -0
  100. package/dist/_internal/shared/listing-types/standard/og.d.ts +1 -0
  101. package/dist/_internal/shared/listing-types/standard/og.js +1 -0
  102. package/dist/_internal/shared/listing-types/standard/schema.d.ts +1 -0
  103. package/dist/_internal/shared/listing-types/standard/schema.js +1 -0
  104. package/dist/_internal/shared/listing-types/standard/seed-factory.d.ts +1 -0
  105. package/dist/_internal/shared/listing-types/standard/seed-factory.js +1 -0
  106. package/dist/_internal/shared/media/limits.d.ts +33 -0
  107. package/dist/_internal/shared/media/limits.js +97 -0
  108. package/dist/_internal/shared/schema-versions.d.ts +76 -0
  109. package/dist/_internal/shared/schema-versions.js +82 -0
  110. package/dist/client.d.ts +9 -0
  111. package/dist/client.js +7 -0
  112. package/dist/constants/api-endpoints.d.ts +6 -3
  113. package/dist/constants/api-endpoints.js +2 -1
  114. package/dist/errors/messages.d.ts +1 -1
  115. package/dist/errors/messages.js +1 -1
  116. package/dist/features/account/migrations.d.ts +2 -0
  117. package/dist/features/account/migrations.js +10 -0
  118. package/dist/features/admin/components/AdminMediaView.js +1 -1
  119. package/dist/features/admin/components/AdminProductsView.js +7 -3
  120. package/dist/features/admin/migrations.d.ts +2 -0
  121. package/dist/features/admin/migrations.js +10 -0
  122. package/dist/features/admin/types/product.types.d.ts +1 -1
  123. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +1 -1
  124. package/dist/features/auctions/migrations.d.ts +2 -0
  125. package/dist/features/auctions/migrations.js +10 -0
  126. package/dist/features/auctions/schemas/index.d.ts +3 -3
  127. package/dist/features/auctions/schemas/index.js +1 -1
  128. package/dist/features/auth/migrations.d.ts +2 -0
  129. package/dist/features/auth/migrations.js +10 -0
  130. package/dist/features/blog/migrations.d.ts +2 -0
  131. package/dist/features/blog/migrations.js +10 -0
  132. package/dist/features/brands/migrations.d.ts +2 -0
  133. package/dist/features/brands/migrations.js +10 -0
  134. package/dist/features/bundles/components/BundlesByCategoryListing.d.ts +6 -0
  135. package/dist/features/bundles/components/BundlesByCategoryListing.js +50 -0
  136. package/dist/features/bundles/components/index.d.ts +2 -0
  137. package/dist/features/bundles/components/index.js +1 -0
  138. package/dist/features/bundles/migrations.d.ts +2 -0
  139. package/dist/features/bundles/migrations.js +10 -0
  140. package/dist/features/bundles/schemas/index.d.ts +1 -0
  141. package/dist/features/bundles/schemas/index.js +1 -0
  142. package/dist/features/bundles/schemas/zod.d.ts +377 -0
  143. package/dist/features/bundles/schemas/zod.js +71 -0
  144. package/dist/features/cart/migrations.d.ts +2 -0
  145. package/dist/features/cart/migrations.js +10 -0
  146. package/dist/features/cart/schemas/firestore.d.ts +2 -2
  147. package/dist/features/categories/components/BrandDetailPageView.js +35 -4
  148. package/dist/features/categories/components/BrandDetailTabs.d.ts +5 -1
  149. package/dist/features/categories/components/BrandDetailTabs.js +22 -8
  150. package/dist/features/categories/components/CategoryBundlesListing.d.ts +6 -0
  151. package/dist/features/categories/components/CategoryBundlesListing.js +74 -0
  152. package/dist/features/categories/components/CategoryDetailPageView.js +29 -4
  153. package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -1
  154. package/dist/features/categories/components/CategoryDetailTabs.js +22 -8
  155. package/dist/features/categories/migrations.d.ts +2 -0
  156. package/dist/features/categories/migrations.js +10 -0
  157. package/dist/features/categories/repository/categories.repository.d.ts +29 -0
  158. package/dist/features/categories/repository/categories.repository.js +83 -0
  159. package/dist/features/categories/schemas/firestore.d.ts +59 -2
  160. package/dist/features/categories/schemas/firestore.js +6 -0
  161. package/dist/features/categories/types/index.d.ts +11 -3
  162. package/dist/features/events/migrations.d.ts +2 -0
  163. package/dist/features/events/migrations.js +10 -0
  164. package/dist/features/faq/migrations.d.ts +2 -0
  165. package/dist/features/faq/migrations.js +10 -0
  166. package/dist/features/grouped/migrations.d.ts +2 -0
  167. package/dist/features/grouped/migrations.js +10 -0
  168. package/dist/features/grouped/schemas/firestore.d.ts +29 -10
  169. package/dist/features/grouped/schemas/firestore.js +10 -5
  170. package/dist/features/history/migrations.d.ts +2 -0
  171. package/dist/features/history/migrations.js +10 -0
  172. package/dist/features/homepage/hooks/useFeaturedAuctions.js +2 -2
  173. package/dist/features/homepage/hooks/useFeaturedPreOrders.js +2 -2
  174. package/dist/features/homepage/lib/section-renderer.js +5 -3
  175. package/dist/features/media/AvatarUpload.js +6 -28
  176. package/dist/features/media/hooks/useMedia.d.ts +31 -15
  177. package/dist/features/media/hooks/useMedia.js +48 -13
  178. package/dist/features/media/upload/ImageUpload.js +1 -1
  179. package/dist/features/media/upload/MediaUploadField.js +1 -1
  180. package/dist/features/messages/migrations.d.ts +2 -0
  181. package/dist/features/messages/migrations.js +10 -0
  182. package/dist/features/orders/components/OrdersList.js +10 -1
  183. package/dist/features/orders/migrations.d.ts +2 -0
  184. package/dist/features/orders/migrations.js +10 -0
  185. package/dist/features/orders/repository/orders.repository.d.ts +16 -0
  186. package/dist/features/orders/repository/orders.repository.js +49 -0
  187. package/dist/features/orders/schemas/firestore.d.ts +8 -0
  188. package/dist/features/orders/types/index.d.ts +12 -0
  189. package/dist/features/orders/utils/order-splitter.d.ts +2 -2
  190. package/dist/features/orders/utils/order-splitter.js +5 -0
  191. package/dist/features/payments/migrations.d.ts +2 -0
  192. package/dist/features/payments/migrations.js +10 -0
  193. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
  194. package/dist/features/products/actions/product-actions.d.ts +1 -1
  195. package/dist/features/products/api/[id]/route.js +34 -0
  196. package/dist/features/products/api/route.js +1 -19
  197. package/dist/features/products/components/CompareOverlay.d.ts +1 -1
  198. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +24 -0
  199. package/dist/features/products/components/MarketplacePrizeDrawCard.js +102 -0
  200. package/dist/features/products/components/PrizeDrawCollage.d.ts +32 -0
  201. package/dist/features/products/components/PrizeDrawCollage.js +22 -0
  202. package/dist/features/products/components/PrizeDrawDetailPageView.d.ts +27 -0
  203. package/dist/features/products/components/PrizeDrawDetailPageView.js +118 -0
  204. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +19 -0
  205. package/dist/features/products/components/PrizeDrawEntryActions.js +48 -0
  206. package/dist/features/products/components/PrizeDrawItemsEditor.d.ts +13 -0
  207. package/dist/features/products/components/PrizeDrawItemsEditor.js +97 -0
  208. package/dist/features/products/components/PrizeDrawsIndexListing.d.ts +8 -0
  209. package/dist/features/products/components/PrizeDrawsIndexListing.js +128 -0
  210. package/dist/features/products/components/PrizeDrawsListingView.d.ts +15 -0
  211. package/dist/features/products/components/PrizeDrawsListingView.js +49 -0
  212. package/dist/features/products/components/PrizeRevealModal.d.ts +34 -0
  213. package/dist/features/products/components/PrizeRevealModal.js +124 -0
  214. package/dist/features/products/components/ProductDetailPageView.js +13 -1
  215. package/dist/features/products/components/ProductForm.js +35 -2
  216. package/dist/features/products/components/ProductGrid.js +3 -1
  217. package/dist/features/products/components/index.d.ts +16 -0
  218. package/dist/features/products/components/index.js +8 -0
  219. package/dist/features/products/constants/listing-tabs.d.ts +113 -0
  220. package/dist/features/products/constants/listing-tabs.js +43 -0
  221. package/dist/features/products/index.d.ts +1 -0
  222. package/dist/features/products/index.js +1 -0
  223. package/dist/features/products/migrations.d.ts +2 -0
  224. package/dist/features/products/migrations.js +10 -0
  225. package/dist/features/products/repository/products.repository.d.ts +11 -7
  226. package/dist/features/products/repository/products.repository.js +49 -24
  227. package/dist/features/products/schemas/firestore.d.ts +3 -3
  228. package/dist/features/products/schemas/firestore.js +2 -2
  229. package/dist/features/products/schemas/index.d.ts +5 -5
  230. package/dist/features/products/schemas/index.js +3 -1
  231. package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
  232. package/dist/features/products/types/index.d.ts +17 -1
  233. package/dist/features/products/utils/listing-type.d.ts +7 -4
  234. package/dist/features/products/utils/listing-type.js +8 -4
  235. package/dist/features/promotions/actions/coupon-actions.d.ts +1 -1
  236. package/dist/features/promotions/hooks/useCouponValidate.d.ts +1 -1
  237. package/dist/features/promotions/migrations.d.ts +2 -0
  238. package/dist/features/promotions/migrations.js +10 -0
  239. package/dist/features/promotions/repository/coupons.repository.d.ts +1 -1
  240. package/dist/features/promotions/schemas/index.d.ts +2 -2
  241. package/dist/features/reviews/migrations.d.ts +2 -0
  242. package/dist/features/reviews/migrations.js +10 -0
  243. package/dist/features/scams/migrations.d.ts +2 -0
  244. package/dist/features/scams/migrations.js +10 -0
  245. package/dist/features/search/api/route.d.ts +1 -1
  246. package/dist/features/search/api/route.js +3 -3
  247. package/dist/features/search/components/Search.d.ts +1 -1
  248. package/dist/features/search/schemas/index.d.ts +3 -3
  249. package/dist/features/search/schemas/index.js +3 -1
  250. package/dist/features/search/types/index.d.ts +2 -2
  251. package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
  252. package/dist/features/seller/components/SellerProductsView.js +20 -6
  253. package/dist/features/seller/migrations.d.ts +2 -0
  254. package/dist/features/seller/migrations.js +10 -0
  255. package/dist/features/seller/schemas/index.d.ts +2 -2
  256. package/dist/features/stores/components/StoreBundlesPageView.d.ts +12 -0
  257. package/dist/features/stores/components/StoreBundlesPageView.js +24 -0
  258. package/dist/features/stores/components/StoreDetailLayoutView.js +15 -3
  259. package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +11 -0
  260. package/dist/features/stores/components/StorePrizeDrawsPageView.js +27 -0
  261. package/dist/features/stores/components/index.d.ts +2 -0
  262. package/dist/features/stores/migrations.d.ts +2 -0
  263. package/dist/features/stores/migrations.js +10 -0
  264. package/dist/features/stores/schemas/index.d.ts +2 -2
  265. package/dist/features/stores/types/index.d.ts +1 -1
  266. package/dist/features/sublisting/migrations.d.ts +2 -0
  267. package/dist/features/sublisting/migrations.js +10 -0
  268. package/dist/features/sublisting/schemas/firestore.d.ts +2 -0
  269. package/dist/features/support/migrations.d.ts +2 -0
  270. package/dist/features/support/migrations.js +10 -0
  271. package/dist/features/wishlist/migrations.d.ts +2 -0
  272. package/dist/features/wishlist/migrations.js +10 -0
  273. package/dist/features/wishlist/types/index.d.ts +1 -1
  274. package/dist/index.d.ts +26 -18
  275. package/dist/index.js +41 -24
  276. package/dist/jobs.d.ts +1 -1
  277. package/dist/jobs.js +4 -0
  278. package/dist/next/api/routeHandler.js +6 -4
  279. package/dist/next/routing/route-map.d.ts +4 -0
  280. package/dist/next/routing/route-map.js +2 -0
  281. package/dist/providers/db-firebase/filter-aliases.d.ts +2 -2
  282. package/dist/repositories/index.d.ts +0 -5
  283. package/dist/repositories/index.js +5 -4
  284. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  285. package/dist/seed/categories-seed-data.js +1105 -6
  286. package/dist/seed/faq-seed-data.js +160 -0
  287. package/dist/seed/grouped-listings-seed-data.js +32 -32
  288. package/dist/seed/homepage-sections-seed-data.js +52 -6
  289. package/dist/seed/index.d.ts +1 -3
  290. package/dist/seed/index.js +4 -3
  291. package/dist/seed/manifest.js +8 -13
  292. package/dist/seed/products-prize-draws-seed-data.d.ts +17 -0
  293. package/dist/seed/products-prize-draws-seed-data.js +313 -0
  294. package/dist/seo/json-ld.d.ts +1 -1
  295. package/dist/server-entry.d.ts +2 -2
  296. package/dist/server-entry.js +5 -3
  297. package/dist/server.d.ts +9 -2
  298. package/dist/server.js +11 -5
  299. package/dist/tailwind-utilities.css +1 -1
  300. package/dist/ui/components/Button.js +21 -2
  301. package/dist/ui/components/Button.style.css +34 -0
  302. package/dist/validation/schemas.d.ts +8 -8
  303. package/package.json +1 -1
  304. package/scripts/seed-cli.mjs +2 -4
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Prize-draw + bundle + maxPerUser gates for the checkout pipeline (SB6-C, SB8-A).
3
+ *
4
+ * Shared helper used by both `createCheckoutOrderAction` (COD/UPI-manual path)
5
+ * and `verifyAndPlaceRazorpayOrderAction` (Razorpay path). Pulled out here so
6
+ * the two transactional flows enforce the same rules.
7
+ */
8
+ import { ValidationError } from "../../../../errors";
9
+ import { orderRepository } from "../../../../repositories";
10
+ /**
11
+ * Pre-transaction read: for every cart item whose product has `maxPerUser`
12
+ * set, count the user's existing active orders and reject the checkout if
13
+ * the new request would push them over the cap.
14
+ *
15
+ * Active = pending / confirmed / processing / shipped / delivered. Cancelled
16
+ * + refunded orders do NOT count (stock was returned to circulation).
17
+ *
18
+ * Throws ValidationError with code `MAX_PER_USER` listing every violating
19
+ * product. The check is informational-rich on purpose — the UI surfaces all
20
+ * blocked items at once instead of revealing them one at a time.
21
+ */
22
+ export async function enforceMaxPerUserForCart(args) {
23
+ const { userId, items } = args;
24
+ // Aggregate per productId so a cart with multiple lines for the same
25
+ // product still gets one accurate count.
26
+ const cartQuantityByProduct = new Map();
27
+ for (const { item } of items) {
28
+ cartQuantityByProduct.set(item.productId, (cartQuantityByProduct.get(item.productId) ?? 0) + item.quantity);
29
+ }
30
+ const violations = [];
31
+ for (const [productId, requested] of cartQuantityByProduct.entries()) {
32
+ const product = items.find((i) => i.item.productId === productId)?.product;
33
+ if (!product)
34
+ continue;
35
+ const cap = product.maxPerUser;
36
+ if (typeof cap !== "number" || cap <= 0)
37
+ continue;
38
+ const alreadyPurchased = await orderRepository.countByUserAndProduct(userId, productId);
39
+ if (alreadyPurchased + requested > cap) {
40
+ violations.push({
41
+ productId,
42
+ productTitle: product.title,
43
+ allowance: cap,
44
+ alreadyPurchased,
45
+ requested,
46
+ });
47
+ }
48
+ }
49
+ if (violations.length > 0) {
50
+ const summary = violations
51
+ .map((v) => `${v.productTitle}: ${v.alreadyPurchased}/${v.allowance} used, requested ${v.requested}`)
52
+ .join("; ");
53
+ throw Object.assign(new ValidationError(`Purchase limit reached for one or more items. ${summary}`), { code: "MAX_PER_USER", violations });
54
+ }
55
+ }
56
+ /**
57
+ * SB8-A — compute the deadline by which the buyer must claim their prize
58
+ * via the reveal API.
59
+ *
60
+ * Rules:
61
+ * - If the reveal window has not yet opened:
62
+ * deadline = min(revealWindowStart + revealDeadlineDays, revealWindowEnd)
63
+ * - If the window is already open:
64
+ * deadline = min(now + revealDeadlineDays, revealWindowEnd)
65
+ *
66
+ * Returns `undefined` if the product lacks the prize-draw fields.
67
+ */
68
+ export function computePrizeRevealDeadline(product, now = new Date()) {
69
+ const windowStart = product.prizeRevealWindowStart
70
+ ? new Date(product.prizeRevealWindowStart)
71
+ : undefined;
72
+ const windowEnd = product.prizeRevealWindowEnd
73
+ ? new Date(product.prizeRevealWindowEnd)
74
+ : undefined;
75
+ if (!windowStart || !windowEnd)
76
+ return undefined;
77
+ const days = product.prizeRevealDeadlineDays ?? 3;
78
+ const dayMs = 86400000;
79
+ const base = windowStart.getTime() > now.getTime() ? windowStart : now;
80
+ const candidate = new Date(base.getTime() + days * dayMs);
81
+ return candidate.getTime() < windowEnd.getTime() ? candidate : windowEnd;
82
+ }
83
+ /**
84
+ * Inside the existing checkout transaction, validate the prize-pool cap for
85
+ * each prize-draw line. Caller must pass the fresh product snapshot read
86
+ * within the transaction (so `prizeCurrentEntries` is up-to-date).
87
+ *
88
+ * Throws ValidationError if any item would overflow the pool.
89
+ */
90
+ export function enforcePrizePoolCap(args) {
91
+ const { productSnapshot, requestedQuantity } = args;
92
+ if (productSnapshot.listingType !== "prize-draw")
93
+ return;
94
+ const max = productSnapshot.prizeMaxEntries ?? 0;
95
+ const current = productSnapshot.prizeCurrentEntries ?? 0;
96
+ if (max > 0 && current + requestedQuantity > max) {
97
+ throw Object.assign(new ValidationError(`Draw is full for "${productSnapshot.title}". ${current}/${max} entries already in.`), { code: "PRIZE_POOL_FULL", productId: productSnapshot.id });
98
+ }
99
+ }
@@ -9,17 +9,24 @@ function mapDoc(doc) {
9
9
  const d = v?.toDate?.();
10
10
  return d ?? (v instanceof Date ? v : new Date());
11
11
  };
12
+ const productIds = Array.isArray(data.productIds)
13
+ ? data.productIds
14
+ : [];
15
+ const minActiveMembers = data.minActiveMembers ?? 2;
16
+ const activeMemberCount = data.activeMemberCount ?? productIds.length;
12
17
  return {
13
18
  id: doc.id,
14
19
  slug: data.slug ?? doc.id,
15
20
  title: data.title ?? "",
16
21
  description: data.description,
17
- productIds: Array.isArray(data.productIds) ? data.productIds : [],
22
+ productIds,
18
23
  coverImage: data.coverImage,
19
- bundlePrice: data.bundlePrice,
20
- originalPrice: data.originalPrice,
21
- discountPercent: data.discountPercent,
22
- currency: (data.currency ?? "INR"),
24
+ // SB-UNI-V — pricing fields dropped; theme-group semantics.
25
+ groupTheme: data.groupTheme ?? "generic",
26
+ minActiveMembers,
27
+ activeMemberCount,
28
+ visibilityStatus: data.visibilityStatus ??
29
+ (activeMemberCount >= minActiveMembers ? "visible" : "hidden"),
23
30
  isActive: data.isActive === true,
24
31
  isFeatured: data.isFeatured === true,
25
32
  storeId: data.storeId,
@@ -10,7 +10,7 @@ export declare const getHomepageInitial: () => Promise<{
10
10
  featuredProducts: never[] | import("../../../..").ProductDocument[];
11
11
  activeAuctions: never[] | import("../../../..").ProductDocument[];
12
12
  activePreOrders: never[] | import("../../../..").ProductDocument[];
13
- activeBrands: never[] | import("../../../..").BrandDocument[];
13
+ activeBrands: never[] | import("../../../..").CategoryDocument[];
14
14
  featuredReviews: never[] | import("../../../..").ReviewDocument[];
15
15
  recentBlogPosts: never[] | import("../../../..").BlogPostDocument[];
16
16
  activeEvents: never[] | import("../../../..").EventDocument[];
@@ -1,5 +1,5 @@
1
1
  import { cache } from "react";
2
- import { homepageSectionsRepository, carouselRepository, productRepository, brandsRepository, reviewRepository, blogRepository, eventRepository, categoriesRepository, } from "../../../../repositories";
2
+ import { homepageSectionsRepository, carouselRepository, productRepository, reviewRepository, blogRepository, eventRepository, categoriesRepository, } from "../../../../repositories";
3
3
  import { HOMEPAGE_FEATURED_REVIEWS_LIMIT, HOMEPAGE_RECENT_BLOG_POSTS_LIMIT, } from "../../../shared/features/homepage/config";
4
4
  export const getHomepageSections = cache(async () => {
5
5
  return homepageSectionsRepository.getEnabledSections().catch(() => []);
@@ -18,7 +18,7 @@ export const getHomepageInitial = cache(async () => {
18
18
  productRepository.findFeatured().catch(() => []),
19
19
  productRepository.findActiveAuctions().catch(() => []),
20
20
  productRepository.findActivePreOrders().catch(() => []),
21
- brandsRepository.findActive().catch(() => []),
21
+ categoriesRepository.findActiveBrands().catch(() => []),
22
22
  reviewRepository.findFeatured(HOMEPAGE_FEATURED_REVIEWS_LIMIT).catch(() => []),
23
23
  blogRepository.listPublished({}, { page: 1, pageSize: HOMEPAGE_RECENT_BLOG_POSTS_LIMIT, sorts: "-publishedAt" }).then((r) => r.items ?? []).catch(() => []),
24
24
  eventRepository.listActive().catch(() => []),
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Media context guardrails — shared by /api/media/sign and the legacy
3
+ * /api/media/upload routes. Validates per-context index caps + image-only
4
+ * vs PDF-only context affinity.
5
+ *
6
+ * Returns either an `{ ok: true }` payload with the derived filename + ext,
7
+ * or `{ ok: false, error, status, details? }` for an error response.
8
+ *
9
+ * Pure logic — no Firebase SDK calls, no `Buffer` work — so it's safe to
10
+ * import from any server route.
11
+ */
12
+ import { type AllowedMime } from "../../../shared/media/limits";
13
+ import { type MediaFilenameContext } from "../../../../utils/id-generators";
14
+ export declare const CONTEXT_LIMITS: {
15
+ readonly PRODUCT_IMAGE_MAX: 5;
16
+ readonly PRODUCT_VIDEO_MAX: 1;
17
+ readonly REVIEW_IMAGE_MAX: 5;
18
+ readonly REVIEW_VIDEO_MAX: 1;
19
+ readonly AUCTION_IMAGE_MAX: 5;
20
+ readonly PREORDER_IMAGE_MAX: 5;
21
+ readonly EVENT_COVER_MAX: 1;
22
+ readonly EVENT_IMAGE_MAX: 10;
23
+ readonly EVENT_WINNER_IMAGE_MAX: 5;
24
+ readonly EVENT_ADDITIONAL_IMAGE_MAX: 10;
25
+ readonly BLOG_COVER_MAX: 1;
26
+ readonly BLOG_CONTENT_IMAGE_MAX: 10;
27
+ readonly BLOG_ADDITIONAL_IMAGE_MAX: 5;
28
+ readonly RICH_TEXT_IMAGE_MAX: 20;
29
+ };
30
+ export interface GuardError {
31
+ ok: false;
32
+ error: string;
33
+ status: number;
34
+ details?: Record<string, unknown>;
35
+ }
36
+ export interface GuardSuccess {
37
+ ok: true;
38
+ filename: string;
39
+ ext: string;
40
+ }
41
+ export type GuardResult = GuardError | GuardSuccess;
42
+ interface GuardInput {
43
+ detectedMime: AllowedMime | string;
44
+ context: MediaFilenameContext;
45
+ }
46
+ /**
47
+ * Apply per-context guardrails to a parsed MediaFilenameContext and
48
+ * derive the SEO filename. Mirrors the inline logic that used to live
49
+ * in /api/media/upload.
50
+ */
51
+ export declare function applyMediaContextGuards({ detectedMime, context: ctx, }: GuardInput): GuardResult;
52
+ export {};
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Media context guardrails — shared by /api/media/sign and the legacy
3
+ * /api/media/upload routes. Validates per-context index caps + image-only
4
+ * vs PDF-only context affinity.
5
+ *
6
+ * Returns either an `{ ok: true }` payload with the derived filename + ext,
7
+ * or `{ ok: false, error, status, details? }` for an error response.
8
+ *
9
+ * Pure logic — no Firebase SDK calls, no `Buffer` work — so it's safe to
10
+ * import from any server route.
11
+ */
12
+ import { classifyMime, MIME_TO_EXT, } from "../../../shared/media/limits";
13
+ import { generateMediaFilename, } from "../../../../utils/id-generators";
14
+ // Per-context limits — mirrored from the previous inline constants in
15
+ // /api/media/upload. Adjust here, not at the call site.
16
+ export const CONTEXT_LIMITS = {
17
+ PRODUCT_IMAGE_MAX: 5,
18
+ PRODUCT_VIDEO_MAX: 1,
19
+ REVIEW_IMAGE_MAX: 5,
20
+ REVIEW_VIDEO_MAX: 1,
21
+ AUCTION_IMAGE_MAX: 5,
22
+ PREORDER_IMAGE_MAX: 5,
23
+ EVENT_COVER_MAX: 1,
24
+ EVENT_IMAGE_MAX: 10,
25
+ EVENT_WINNER_IMAGE_MAX: 5,
26
+ EVENT_ADDITIONAL_IMAGE_MAX: 10,
27
+ BLOG_COVER_MAX: 1,
28
+ BLOG_CONTENT_IMAGE_MAX: 10,
29
+ BLOG_ADDITIONAL_IMAGE_MAX: 5,
30
+ RICH_TEXT_IMAGE_MAX: 20,
31
+ };
32
+ const PDF_ONLY_CONTEXTS = ["invoice", "payout-doc"];
33
+ const IMAGE_ONLY_CONTEXT_TYPES = new Set([
34
+ "store-logo",
35
+ "store-banner",
36
+ "user-avatar",
37
+ "event-cover",
38
+ "event-image",
39
+ "event-winner-image",
40
+ "event-additional-image",
41
+ "blog-cover",
42
+ "blog-content-image",
43
+ "blog-additional-image",
44
+ ]);
45
+ function indexGuard(receivedIndex, max, errorMessage) {
46
+ if (receivedIndex < 1 || receivedIndex > max) {
47
+ return {
48
+ ok: false,
49
+ status: 400,
50
+ error: errorMessage,
51
+ details: { maxImages: max, receivedIndex },
52
+ };
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Apply per-context guardrails to a parsed MediaFilenameContext and
58
+ * derive the SEO filename. Mirrors the inline logic that used to live
59
+ * in /api/media/upload.
60
+ */
61
+ export function applyMediaContextGuards({ detectedMime, context: ctx, }) {
62
+ const kind = classifyMime(detectedMime);
63
+ if (!kind) {
64
+ return {
65
+ ok: false,
66
+ status: 400,
67
+ error: "Unsupported MIME type",
68
+ details: { detected: detectedMime },
69
+ };
70
+ }
71
+ const isVideo = kind === "video";
72
+ const isPdf = kind === "pdf";
73
+ if (ctx.type === "product-image") {
74
+ const idx = ctx.index ?? 1;
75
+ const err = indexGuard(idx, CONTEXT_LIMITS.PRODUCT_IMAGE_MAX, "Product image index exceeds max allowed");
76
+ if (err)
77
+ return err;
78
+ }
79
+ if (ctx.type === "product-video") {
80
+ const idx = ctx.index ?? 1;
81
+ const err = indexGuard(idx, CONTEXT_LIMITS.PRODUCT_VIDEO_MAX, "Only one product video is allowed");
82
+ if (err)
83
+ return err;
84
+ }
85
+ if (ctx.type === "review-image") {
86
+ const idx = ctx.index ?? 1;
87
+ const err = indexGuard(idx, CONTEXT_LIMITS.REVIEW_IMAGE_MAX, "Review image index exceeds max allowed");
88
+ if (err)
89
+ return err;
90
+ }
91
+ if (ctx.type === "review-video" && !isVideo) {
92
+ return {
93
+ ok: false,
94
+ status: 400,
95
+ error: "review-video context requires a video file",
96
+ };
97
+ }
98
+ if (ctx.type === "auction-image") {
99
+ const idx = ctx.index ?? 1;
100
+ const err = indexGuard(idx, CONTEXT_LIMITS.AUCTION_IMAGE_MAX, "Auction image index exceeds max allowed");
101
+ if (err)
102
+ return err;
103
+ }
104
+ if (ctx.type === "preorder-image") {
105
+ const idx = ctx.index ?? 1;
106
+ const err = indexGuard(idx, CONTEXT_LIMITS.PREORDER_IMAGE_MAX, "Pre-order image index exceeds max allowed");
107
+ if (err)
108
+ return err;
109
+ }
110
+ if (ctx.type === "event-cover") {
111
+ const idx = ctx.index ?? 1;
112
+ const err = indexGuard(idx, CONTEXT_LIMITS.EVENT_COVER_MAX, "Only one event cover image is allowed");
113
+ if (err)
114
+ return err;
115
+ }
116
+ if (ctx.type === "event-image") {
117
+ const idx = ctx.index ?? 1;
118
+ const err = indexGuard(idx, CONTEXT_LIMITS.EVENT_IMAGE_MAX, "Event image index exceeds max allowed");
119
+ if (err)
120
+ return err;
121
+ }
122
+ if (ctx.type === "event-winner-image") {
123
+ const idx = ctx.index ?? 1;
124
+ const err = indexGuard(idx, CONTEXT_LIMITS.EVENT_WINNER_IMAGE_MAX, "Event winner image index exceeds max allowed");
125
+ if (err)
126
+ return err;
127
+ }
128
+ if (ctx.type === "event-additional-image") {
129
+ const idx = ctx.index ?? 1;
130
+ const err = indexGuard(idx, CONTEXT_LIMITS.EVENT_ADDITIONAL_IMAGE_MAX, "Event additional image index exceeds max allowed");
131
+ if (err)
132
+ return err;
133
+ }
134
+ if (ctx.type === "blog-cover") {
135
+ const idx = ctx.index ?? 1;
136
+ const err = indexGuard(idx, CONTEXT_LIMITS.BLOG_COVER_MAX, "Only one blog cover image is allowed");
137
+ if (err)
138
+ return err;
139
+ }
140
+ if (ctx.type === "blog-content-image") {
141
+ const idx = ctx.index ?? 1;
142
+ const err = indexGuard(idx, CONTEXT_LIMITS.BLOG_CONTENT_IMAGE_MAX, "Blog content image index exceeds max allowed");
143
+ if (err)
144
+ return err;
145
+ }
146
+ if (ctx.type === "blog-additional-image") {
147
+ const idx = ctx.index ?? 1;
148
+ const err = indexGuard(idx, CONTEXT_LIMITS.BLOG_ADDITIONAL_IMAGE_MAX, "Blog additional image index exceeds max allowed");
149
+ if (err)
150
+ return err;
151
+ }
152
+ if (ctx.type === "rich-text-image") {
153
+ if (isVideo) {
154
+ return {
155
+ ok: false,
156
+ status: 400,
157
+ error: "rich-text-image context requires an image file",
158
+ };
159
+ }
160
+ const idx = ctx.index ?? 1;
161
+ const err = indexGuard(idx, CONTEXT_LIMITS.RICH_TEXT_IMAGE_MAX, "Rich text image index exceeds max allowed");
162
+ if (err)
163
+ return err;
164
+ }
165
+ if (IMAGE_ONLY_CONTEXT_TYPES.has(ctx.type) && isVideo) {
166
+ return {
167
+ ok: false,
168
+ status: 400,
169
+ error: `${ctx.type} must be an image file, not a video`,
170
+ };
171
+ }
172
+ const isPdfOnlyContext = (c) => PDF_ONLY_CONTEXTS.includes(c.type);
173
+ const ext = MIME_TO_EXT[detectedMime] ?? "bin";
174
+ if (isPdfOnlyContext(ctx)) {
175
+ if (!isPdf) {
176
+ return {
177
+ ok: false,
178
+ status: 400,
179
+ error: `${ctx.type} context requires a PDF file`,
180
+ details: { context: ctx.type, detected: detectedMime },
181
+ };
182
+ }
183
+ return { ok: true, filename: generateMediaFilename(ctx), ext };
184
+ }
185
+ if (isPdf) {
186
+ return {
187
+ ok: false,
188
+ status: 400,
189
+ error: "PDF uploads are only allowed for invoice or payout-doc contexts",
190
+ details: { context: ctx.type, detected: detectedMime },
191
+ };
192
+ }
193
+ return {
194
+ ok: true,
195
+ filename: generateMediaFilename({ ...ctx, ext }),
196
+ ext,
197
+ };
198
+ }
@@ -7,6 +7,18 @@ export function orderDocumentToOrder(doc) {
7
7
  quantity: item.quantity,
8
8
  currency: doc.currency,
9
9
  storeId: doc.storeId,
10
+ // SB8-F — surface prize-draw reveal-state to the API response so
11
+ // user-orders pages can render the "X reveals pending" badge.
12
+ ...(item.listingType ? { listingType: item.listingType } : {}),
13
+ ...(item.prizeRevealStatus
14
+ ? { prizeRevealStatus: item.prizeRevealStatus }
15
+ : {}),
16
+ ...(item.prizeRevealDeadline
17
+ ? { prizeRevealDeadline: item.prizeRevealDeadline }
18
+ : {}),
19
+ ...(item.revealedItemNumber != null
20
+ ? { revealedItemNumber: item.revealedItemNumber }
21
+ : {}),
10
22
  }))
11
23
  : [
12
24
  {
@@ -8,7 +8,7 @@ export interface SitemapProduct {
8
8
  slugOrId: string;
9
9
  updatedAt: Date;
10
10
  /** Canonical listing-kind discriminator (SB1-G — Phase 4 dropped booleans). */
11
- listingType: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle";
11
+ listingType: "standard" | "auction" | "pre-order" | "prize-draw";
12
12
  }
13
13
  /** List all published products for sitemap generation. */
14
14
  export declare function listSitemapProducts(): Promise<SitemapProduct[]>;
@@ -1 +1 @@
1
- export declare const getSublistingCategoryForDetail: (slug: string) => Promise<import("../../../..").SublistingCategoryDocument | null>;
1
+ export declare const getSublistingCategoryForDetail: (slug: string) => Promise<import("../../../..").CategoryDocument | null>;
@@ -1,5 +1,5 @@
1
1
  import { cache } from "react";
2
- import { sublistingCategoriesRepository } from "../../../../repositories";
2
+ import { categoriesRepository } from "../../../../repositories";
3
3
  export const getSublistingCategoryForDetail = cache(async (slug) => {
4
- return sublistingCategoriesRepository.findBySlug(slug).catch(() => null);
4
+ return categoriesRepository.findBySlugAndType(slug, "sublisting").catch(() => null);
5
5
  });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * assignSpinPrize — HTTP callable (SB9-E).
3
+ *
4
+ * Weighted random selection from an event's `spinPrizes` config using
5
+ * `crypto.randomInt`. Enforces one spin per user (checks `spinUsed=true` on
6
+ * the user's event entry). Sets `spinPrizeId` + `spinWonAt` on the entry doc.
7
+ * Issues a coupon code if the prize has `spinPrizeCouponId` set.
8
+ */
9
+ import type { CallableHandler } from "../runtime/types";
10
+ interface AssignSpinPrizeInput {
11
+ eventId: string;
12
+ userId: string;
13
+ }
14
+ interface AssignSpinPrizeResult {
15
+ eventId: string;
16
+ userId: string;
17
+ spinPrizeId?: string;
18
+ spinPrizeTitle?: string;
19
+ spinPrizeCouponCode?: string;
20
+ alreadyUsed?: boolean;
21
+ reason?: string;
22
+ }
23
+ export declare const assignSpinPrizeHandler: CallableHandler<AssignSpinPrizeInput, AssignSpinPrizeResult>;
24
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * assignSpinPrize — HTTP callable (SB9-E).
3
+ *
4
+ * Weighted random selection from an event's `spinPrizes` config using
5
+ * `crypto.randomInt`. Enforces one spin per user (checks `spinUsed=true` on
6
+ * the user's event entry). Sets `spinPrizeId` + `spinWonAt` on the entry doc.
7
+ * Issues a coupon code if the prize has `spinPrizeCouponId` set.
8
+ */
9
+ import crypto from "node:crypto";
10
+ const EVENTS_COLLECTION = "events";
11
+ const EVENT_ENTRIES_COLLECTION = "eventEntries";
12
+ const COUPONS_COLLECTION = "coupons";
13
+ export const assignSpinPrizeHandler = async (input, ctx) => {
14
+ const { eventId, userId } = input;
15
+ const eventSnap = await ctx.db.collection(EVENTS_COLLECTION).doc(eventId).get();
16
+ if (!eventSnap.exists) {
17
+ return { eventId, userId, reason: "event_not_found" };
18
+ }
19
+ const event = eventSnap.data();
20
+ const prizes = Array.isArray(event.spinPrizes) ? event.spinPrizes : [];
21
+ const totalWeight = prizes.reduce((s, p) => s + (p.weight ?? 0), 0);
22
+ if (totalWeight <= 0) {
23
+ return { eventId, userId, reason: "no_prizes_configured" };
24
+ }
25
+ const entriesSnap = await ctx.db
26
+ .collection(EVENT_ENTRIES_COLLECTION)
27
+ .where("eventId", "==", eventId)
28
+ .where("userId", "==", userId)
29
+ .limit(1)
30
+ .get();
31
+ if (entriesSnap.empty) {
32
+ return { eventId, userId, reason: "entry_not_found" };
33
+ }
34
+ const entryDoc = entriesSnap.docs[0];
35
+ const entry = entryDoc.data();
36
+ if (entry.spinUsed) {
37
+ return {
38
+ eventId,
39
+ userId,
40
+ spinPrizeId: entry.spinPrizeId,
41
+ alreadyUsed: true,
42
+ };
43
+ }
44
+ // Weighted pick: roll a uniform integer in [0, totalWeight) and walk.
45
+ const roll = crypto.randomInt(0, totalWeight);
46
+ let cumulative = 0;
47
+ let pick;
48
+ for (const p of prizes) {
49
+ cumulative += p.weight ?? 0;
50
+ if (roll < cumulative) {
51
+ pick = p;
52
+ break;
53
+ }
54
+ }
55
+ if (!pick)
56
+ pick = prizes[prizes.length - 1];
57
+ let spinPrizeCouponCode;
58
+ if (pick.couponId) {
59
+ const coupSnap = await ctx.db
60
+ .collection(COUPONS_COLLECTION)
61
+ .doc(pick.couponId)
62
+ .get();
63
+ if (coupSnap.exists) {
64
+ spinPrizeCouponCode = coupSnap.data().code;
65
+ }
66
+ }
67
+ await entryDoc.ref.update({
68
+ spinUsed: true,
69
+ spinPrizeId: pick.id,
70
+ spinWonAt: ctx.now,
71
+ spinPrizeCouponCode: spinPrizeCouponCode ?? null,
72
+ updatedAt: ctx.now,
73
+ });
74
+ ctx.logger.info("Spin prize assigned", {
75
+ eventId,
76
+ userId,
77
+ spinPrizeId: pick.id,
78
+ });
79
+ return {
80
+ eventId,
81
+ userId,
82
+ spinPrizeId: pick.id,
83
+ spinPrizeTitle: pick.title,
84
+ spinPrizeCouponCode,
85
+ };
86
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * scheduledBundleStockSync — daily 10am IST safety net.
3
+ *
4
+ * SB-UNI-V: bundles live on the `categories` collection with
5
+ * categoryType:"bundle". Each bundle row owns `bundleProductIds[]` +
6
+ * `bundleStockStatus`. This sweep recomputes `bundleStockStatus` for
7
+ * every active bundle by inspecting its members' product status.
8
+ *
9
+ * Status transitions:
10
+ * all members published + available → "in_stock"
11
+ * ≥1 member unavailable, ≥minActive → "partial"
12
+ * ≥1 member unavailable, <minActive → "out_of_stock"
13
+ *
14
+ * The realtime onProductStockChange Firestore-trigger handles individual
15
+ * writes; this is the cheap nightly safety net for any missed events.
16
+ */
17
+ import type { ScheduleHandler } from "../runtime/types";
18
+ export declare const bundleStockSyncHandler: ScheduleHandler;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * scheduledBundleStockSync — daily 10am IST safety net.
3
+ *
4
+ * SB-UNI-V: bundles live on the `categories` collection with
5
+ * categoryType:"bundle". Each bundle row owns `bundleProductIds[]` +
6
+ * `bundleStockStatus`. This sweep recomputes `bundleStockStatus` for
7
+ * every active bundle by inspecting its members' product status.
8
+ *
9
+ * Status transitions:
10
+ * all members published + available → "in_stock"
11
+ * ≥1 member unavailable, ≥minActive → "partial"
12
+ * ≥1 member unavailable, <minActive → "out_of_stock"
13
+ *
14
+ * The realtime onProductStockChange Firestore-trigger handles individual
15
+ * writes; this is the cheap nightly safety net for any missed events.
16
+ */
17
+ const CATEGORIES_COLLECTION = "categories";
18
+ const PRODUCT_COLLECTION = "products";
19
+ const UNAVAILABLE = new Set(["sold", "out_of_stock", "discontinued"]);
20
+ const MIN_ACTIVE_DEFAULT = 1;
21
+ async function computeBundleStockStatus(productIds, minActive, ctx) {
22
+ if (productIds.length === 0)
23
+ return "out_of_stock";
24
+ let unavailable = 0;
25
+ for (let i = 0; i < productIds.length; i += 30) {
26
+ const chunk = productIds.slice(i, i + 30);
27
+ const snap = await ctx.db
28
+ .collection(PRODUCT_COLLECTION)
29
+ .where("__name__", "in", chunk)
30
+ .get();
31
+ for (const doc of snap.docs) {
32
+ const status = doc.data().status;
33
+ if (!status || UNAVAILABLE.has(status))
34
+ unavailable++;
35
+ }
36
+ // Account for missing docs (deleted products).
37
+ if (snap.size < chunk.length)
38
+ unavailable += chunk.length - snap.size;
39
+ }
40
+ const active = productIds.length - unavailable;
41
+ if (unavailable === 0)
42
+ return "in_stock";
43
+ if (active < minActive)
44
+ return "out_of_stock";
45
+ return "partial";
46
+ }
47
+ export const bundleStockSyncHandler = async (ctx) => {
48
+ ctx.logger.info("Bundle stock sync starting (SB-UNI-V categories)");
49
+ const snap = await ctx.db
50
+ .collection(CATEGORIES_COLLECTION)
51
+ .where("categoryType", "==", "bundle")
52
+ .where("isActive", "==", true)
53
+ .limit(500)
54
+ .get();
55
+ if (snap.empty) {
56
+ ctx.logger.info("No active bundle categories to scan");
57
+ return;
58
+ }
59
+ let updated = 0;
60
+ for (const bundleDoc of snap.docs) {
61
+ const data = bundleDoc.data();
62
+ const productIds = data.bundleProductIds ?? [];
63
+ if (productIds.length === 0)
64
+ continue;
65
+ const minActive = data.minActiveMembers ?? MIN_ACTIVE_DEFAULT;
66
+ const nextStatus = await computeBundleStockStatus(productIds, minActive, ctx);
67
+ if (nextStatus !== data.bundleStockStatus) {
68
+ await bundleDoc.ref.update({
69
+ bundleStockStatus: nextStatus,
70
+ bundleQueryResolvedAt: ctx.now,
71
+ updatedAt: ctx.now,
72
+ });
73
+ updated++;
74
+ }
75
+ }
76
+ ctx.logger.info("Bundle stock sync complete", {
77
+ scanned: snap.size,
78
+ updated,
79
+ });
80
+ };
@@ -33,3 +33,11 @@ export { onStoreWriteHandler } from "./onStoreWrite";
33
33
  export { adminAnalyticsHandler, type AdminAnalyticsResult } from "./adminAnalytics";
34
34
  export { storeAnalyticsHandler, type StoreAnalyticsInput, type StoreAnalyticsResult, } from "./storeAnalytics";
35
35
  export { listingProcessorHandler, supportedListingCollections, type ListingRequestBody, type ListingResponseBody, } from "./listingProcessor";
36
+ export { prizeRevealOpenHandler } from "./prizeRevealOpen";
37
+ export { prizeRevealCloseHandler } from "./prizeRevealClose";
38
+ export { prizeRevealExpiryHandler } from "./prizeRevealExpiry";
39
+ export { prizeRevealReminderHandler } from "./prizeRevealReminder";
40
+ export { bundleStockSyncHandler } from "./bundleStockSync";
41
+ export { onProductStockChangeHandler } from "./onProductStockChange";
42
+ export { triggerEventRaffleHandler } from "./triggerEventRaffle";
43
+ export { assignSpinPrizeHandler } from "./assignSpinPrize";