@mohasinac/appkit 4.1.3 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/dist/_internal/server/features/auth/actions.d.ts +0 -1
  2. package/dist/_internal/server/features/auth/actions.js +0 -1
  3. package/dist/_internal/server/features/bundles/data.d.ts +22 -4
  4. package/dist/_internal/server/features/bundles/data.js +41 -4
  5. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  6. package/dist/_internal/server/features/bundles/index.js +1 -1
  7. package/dist/_internal/server/features/checkout/actions.d.ts +15 -0
  8. package/dist/_internal/server/features/checkout/actions.js +249 -257
  9. package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
  10. package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
  11. package/dist/_internal/server/features/orders/adapters.js +1 -14
  12. package/dist/_internal/server/features/products/actions.js +8 -3
  13. package/dist/_internal/server/features/products/service.d.ts +15 -0
  14. package/dist/_internal/server/features/products/service.js +41 -0
  15. package/dist/_internal/server/functions/firestore.d.ts +3 -1
  16. package/dist/_internal/server/functions/firestore.js +17 -1
  17. package/dist/_internal/server/functions/scheduled.d.ts +3 -5
  18. package/dist/_internal/server/functions/scheduled.js +14 -30
  19. package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
  20. package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
  21. package/dist/_internal/server/jobs/core/index.d.ts +4 -4
  22. package/dist/_internal/server/jobs/core/index.js +4 -4
  23. package/dist/_internal/server/jobs/core/jobRunners.js +10 -0
  24. package/dist/_internal/server/jobs/core/newsletterExport.d.ts +15 -0
  25. package/dist/_internal/server/jobs/core/newsletterExport.js +43 -0
  26. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
  27. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
  28. package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
  29. package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
  30. package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
  31. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +3 -2
  32. package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
  33. package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
  34. package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
  35. package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
  36. package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
  37. package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
  38. package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
  39. package/dist/_internal/server/jobs/core/revenueRollup.d.ts +13 -0
  40. package/dist/_internal/server/jobs/core/revenueRollup.js +22 -0
  41. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
  42. package/dist/_internal/server/jobs/core/whatsappNotify.js +82 -0
  43. package/dist/_internal/server/jobs/handlers/index.d.ts +4 -4
  44. package/dist/_internal/server/jobs/handlers/index.js +9 -6
  45. package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
  46. package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
  47. package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
  48. package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
  49. package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
  50. package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
  51. package/dist/_internal/server/jobs/handlers/revenueRollup.d.ts +2 -0
  52. package/dist/_internal/server/jobs/handlers/revenueRollup.js +2 -0
  53. package/dist/_internal/shared/actions/action-registry.js +40 -10
  54. package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
  55. package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
  56. package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
  57. package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
  58. package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
  59. package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
  60. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  61. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  62. package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
  63. package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
  64. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
  65. package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
  66. package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
  67. package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
  68. package/dist/_internal/shared/features/checkout/config.d.ts +7 -0
  69. package/dist/_internal/shared/features/checkout/config.js +7 -0
  70. package/dist/_internal/shared/fees/calculator.d.ts +26 -0
  71. package/dist/_internal/shared/fees/calculator.js +25 -0
  72. package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
  73. package/dist/constants/api-endpoints.d.ts +6 -9
  74. package/dist/constants/api-endpoints.js +5 -5
  75. package/dist/constants/field-names.d.ts +17 -1
  76. package/dist/constants/field-names.js +17 -1
  77. package/dist/features/about/components/FeesView.js +18 -0
  78. package/dist/features/account/hooks/useNotifications.js +5 -0
  79. package/dist/features/admin/actions/admin-actions.js +6 -0
  80. package/dist/features/admin/actions/notification-actions.d.ts +4 -1
  81. package/dist/features/admin/actions/notification-actions.js +57 -12
  82. package/dist/features/admin/components/AdminAdsView.js +1 -1
  83. package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
  84. package/dist/features/admin/components/AdminNewsletterView.js +42 -11
  85. package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
  86. package/dist/features/admin/components/AdminSiteSettingsView.js +115 -11
  87. package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
  88. package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
  89. package/dist/features/admin/hooks/useAdminListing.js +4 -2
  90. package/dist/features/admin/repository/analytics-rollup.repository.d.ts +25 -0
  91. package/dist/features/admin/repository/analytics-rollup.repository.js +31 -0
  92. package/dist/features/admin/repository/site-settings.repository.js +8 -1
  93. package/dist/features/admin/schemas/firestore.d.ts +54 -5
  94. package/dist/features/admin/schemas/firestore.js +63 -26
  95. package/dist/features/auctions/actions/bid-actions.js +3 -1
  96. package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
  97. package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
  98. package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
  99. package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
  100. package/dist/features/auctions/components/CollapsibleBidHistory.js +21 -3
  101. package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
  102. package/dist/features/auctions/components/LiveBidPrice.js +17 -0
  103. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
  104. package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
  105. package/dist/features/auctions/hooks/useBids.d.ts +33 -0
  106. package/dist/features/auctions/hooks/useBids.js +32 -0
  107. package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
  108. package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
  109. package/dist/features/auth/checkout-value-otp.d.ts +7 -10
  110. package/dist/features/auth/checkout-value-otp.js +14 -10
  111. package/dist/features/auth/server.d.ts +0 -1
  112. package/dist/features/auth/server.js +0 -1
  113. package/dist/features/blog/api/[slug]/route.d.ts +5 -0
  114. package/dist/features/blog/api/[slug]/route.js +12 -3
  115. package/dist/features/blog/components/BlogPostView.d.ts +2 -0
  116. package/dist/features/blog/components/BlogPostView.js +5 -4
  117. package/dist/features/blog/hooks/useBlog.d.ts +2 -0
  118. package/dist/features/blog/hooks/useBlog.js +2 -0
  119. package/dist/features/blog/repository/blog.repository.d.ts +7 -0
  120. package/dist/features/blog/repository/blog.repository.js +44 -0
  121. package/dist/features/blog/schemas/firestore.d.ts +3 -0
  122. package/dist/features/blog/schemas/firestore.js +1 -0
  123. package/dist/features/blog/types/index.d.ts +2 -0
  124. package/dist/features/cart/components/index.d.ts +0 -2
  125. package/dist/features/cart/components/index.js +0 -1
  126. package/dist/features/cart/hooks/useCartCount.js +22 -5
  127. package/dist/features/cart/hooks/useGuestCart.js +11 -1
  128. package/dist/features/cart/utils/guest-cart.d.ts +4 -0
  129. package/dist/features/cart/utils/guest-cart.js +10 -0
  130. package/dist/features/cart/utils/pending-ops.d.ts +17 -0
  131. package/dist/features/cart/utils/pending-ops.js +46 -5
  132. package/dist/features/categories/components/BrandDetailPageView.js +6 -2
  133. package/dist/features/categories/components/BundleDetailView.js +4 -2
  134. package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
  135. package/dist/features/categories/schemas/firestore.d.ts +7 -0
  136. package/dist/features/checkout/actions/index.d.ts +0 -1
  137. package/dist/features/checkout/actions/index.js +0 -1
  138. package/dist/features/checkout/schemas/firestore.d.ts +2 -2
  139. package/dist/features/events/actions/event-actions.d.ts +2 -0
  140. package/dist/features/events/actions/event-actions.js +5 -0
  141. package/dist/features/events/components/EventCard.js +1 -2
  142. package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
  143. package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
  144. package/dist/features/events/components/index.d.ts +2 -0
  145. package/dist/features/events/components/index.js +1 -0
  146. package/dist/features/events/repository/events.repository.d.ts +6 -0
  147. package/dist/features/events/repository/events.repository.js +21 -0
  148. package/dist/features/history/repository/user-history.repository.d.ts +1 -1
  149. package/dist/features/history/utils/guest-history.d.ts +1 -1
  150. package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
  151. package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
  152. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  153. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  154. package/dist/features/layout/AppLayoutShell.js +1 -1
  155. package/dist/features/layout/BackToTop.d.ts +9 -5
  156. package/dist/features/layout/BackToTop.js +17 -21
  157. package/dist/features/layout/NavbarLayout.js +1 -1
  158. package/dist/features/layout/TitleBarLayout.js +2 -2
  159. package/dist/features/media/MediaVideo.d.ts +7 -0
  160. package/dist/features/media/MediaVideo.js +36 -5
  161. package/dist/features/orders/actions/refund-actions.js +1 -0
  162. package/dist/features/orders/components/OrdersList.js +1 -5
  163. package/dist/features/orders/schemas/firestore.d.ts +16 -8
  164. package/dist/features/orders/schemas/index.d.ts +3 -32
  165. package/dist/features/orders/schemas/index.js +1 -4
  166. package/dist/features/orders/types/index.d.ts +6 -9
  167. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  168. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
  169. package/dist/features/products/actions/product-actions.js +13 -10
  170. package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
  171. package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
  172. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  173. package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
  174. package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
  175. package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
  176. package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
  177. package/dist/features/products/components/PrizeRevealModal.js +18 -147
  178. package/dist/features/products/components/ProductDetailPageView.js +72 -30
  179. package/dist/features/products/components/ProductForm.js +7 -14
  180. package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
  181. package/dist/features/products/components/ProductGalleryClient.js +29 -10
  182. package/dist/features/products/components/ProductsIndexListing.js +2 -1
  183. package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
  184. package/dist/features/products/components/ShowGroupSection.js +2 -2
  185. package/dist/features/products/components/SublistingCarouselSection.js +1 -1
  186. package/dist/features/products/components/index.d.ts +4 -2
  187. package/dist/features/products/components/index.js +1 -0
  188. package/dist/features/products/constants/action-defs.d.ts +28 -4
  189. package/dist/features/products/constants/action-defs.js +76 -3
  190. package/dist/features/products/repository/products.repository.d.ts +6 -0
  191. package/dist/features/products/repository/products.repository.js +20 -1
  192. package/dist/features/products/schemas/firestore.d.ts +26 -2
  193. package/dist/features/products/schemas/index.d.ts +11 -3
  194. package/dist/features/products/schemas/index.js +10 -2
  195. package/dist/features/products/types/index.d.ts +2 -1
  196. package/dist/features/reviews/actions/review-actions.js +1 -0
  197. package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
  198. package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
  199. package/dist/features/reviews/components/ReviewsList.js +2 -1
  200. package/dist/features/reviews/schemas/firestore.d.ts +3 -0
  201. package/dist/features/reviews/schemas/index.d.ts +6 -0
  202. package/dist/features/reviews/schemas/index.js +2 -0
  203. package/dist/features/reviews/types/index.d.ts +3 -0
  204. package/dist/features/scams/actions/scam-actions.d.ts +3 -0
  205. package/dist/features/scams/actions/scam-actions.js +3 -2
  206. package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
  207. package/dist/features/scams/components/ScamProfileView.js +2 -2
  208. package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
  209. package/dist/features/scams/repository/scammer.repository.js +24 -0
  210. package/dist/features/seller/actions/seller-actions.js +8 -0
  211. package/dist/features/seller/components/SellerOrdersView.js +1 -1
  212. package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
  213. package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
  214. package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
  215. package/dist/features/stores/components/StoreNavTabs.js +21 -9
  216. package/dist/features/tester/actions/index.d.ts +1 -0
  217. package/dist/features/tester/actions/index.js +1 -0
  218. package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
  219. package/dist/features/tester/actions/leaderboard-actions.js +5 -0
  220. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
  221. package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
  222. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
  223. package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
  224. package/dist/features/tester/components/index.d.ts +2 -0
  225. package/dist/features/tester/components/index.js +1 -0
  226. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
  227. package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
  228. package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
  229. package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
  230. package/dist/features/tester/schemas/firestore.d.ts +22 -1
  231. package/dist/features/tester/schemas/firestore.js +8 -0
  232. package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
  233. package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
  234. package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
  235. package/dist/features/tester/seed-data/products-tester-seed-data.js +152 -8
  236. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +249 -14
  237. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +10 -1
  238. package/dist/features/whatsapp-bot/helpers/whatsapp.js +47 -0
  239. package/dist/features/whatsapp-bot/types/index.d.ts +19 -0
  240. package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
  241. package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
  242. package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
  243. package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
  244. package/dist/index.d.ts +23 -24
  245. package/dist/index.js +29 -60
  246. package/dist/jobs.d.ts +1 -1
  247. package/dist/jobs.js +2 -2
  248. package/dist/next/routing/route-map.d.ts +4 -0
  249. package/dist/next/routing/route-map.js +2 -0
  250. package/dist/repositories/index.d.ts +2 -0
  251. package/dist/repositories/index.js +1 -0
  252. package/dist/schemas/registry.d.ts +6 -26
  253. package/dist/seed/bids-seed-data.js +14 -2
  254. package/dist/seed/blog-posts-seed-data.js +2 -1
  255. package/dist/seed/categories-seed-data.js +11 -2
  256. package/dist/seed/events-seed-data.js +11 -11
  257. package/dist/seed/faq-seed-data.js +5 -1
  258. package/dist/seed/products-auctions-seed-data.js +2 -2
  259. package/dist/seed/products-preorders-seed-data.d.ts +2 -1
  260. package/dist/seed/products-prize-draws-seed-data.js +56 -10
  261. package/dist/seed/products-standard-seed-data.js +82 -3
  262. package/dist/seed/scammers-seed-data.js +75 -2
  263. package/dist/seed/site-settings-seed-data.js +12 -2
  264. package/dist/server-entry.d.ts +0 -1
  265. package/dist/server-entry.js +0 -1
  266. package/dist/server.d.ts +6 -22
  267. package/dist/server.js +12 -59
  268. package/dist/styles.css +1 -1
  269. package/dist/tailwind-utilities.css +1 -1
  270. package/dist/ui/components/BaseListingCard.js +1 -1
  271. package/dist/ui/components/ImageLightbox.d.ts +9 -0
  272. package/dist/ui/components/ImageLightbox.js +6 -3
  273. package/dist/ui/components/ListingToolbar.d.ts +18 -1
  274. package/dist/ui/components/ListingToolbar.js +5 -2
  275. package/dist/ui/components/StickyToolbar.js +13 -8
  276. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  /**
2
- * maxPerUser gate + prize reveal deadline helper for the checkout pipeline
3
- * (SB6-C, SB8-A).
2
+ * maxPerUser gate for the checkout pipeline (SB6-C).
4
3
  *
5
4
  * `enforcePrizePoolCap` was removed in S-SBUNI-RULES (2026-05-13) — the
6
5
  * equivalent check now lives in `prizeDrawRule.preflightChecks` in the rule
7
- * registry and is invoked via `runSyncPreflight`.
6
+ * registry and is invoked via `runSyncPreflight`. `computePrizeRevealDeadline`
7
+ * (SB8-A) was removed with the buyer-claim-deadline mechanic — reveal is now
8
+ * fully automatic, there's nothing left for a buyer to "claim" before a
9
+ * deadline.
8
10
  */
9
11
  import type { ProductDocument } from "../../../../features/products/schemas/firestore";
10
12
  import type { CartItemDocument } from "../../../../features/cart/schemas/firestore";
@@ -34,16 +36,3 @@ export declare function enforceMaxPerUserForCart(args: {
34
36
  product: ProductDocument;
35
37
  }>;
36
38
  }): Promise<void>;
37
- /**
38
- * SB8-A — compute the deadline by which the buyer must claim their prize
39
- * via the reveal API.
40
- *
41
- * Rules:
42
- * - If the reveal window has not yet opened:
43
- * deadline = min(revealWindowStart + revealDeadlineDays, revealWindowEnd)
44
- * - If the window is already open:
45
- * deadline = min(now + revealDeadlineDays, revealWindowEnd)
46
- *
47
- * Returns `undefined` if the product lacks the prize-draw fields.
48
- */
49
- export declare function computePrizeRevealDeadline(product: Pick<ProductDocument, "prizeRevealWindowStart" | "prizeRevealWindowEnd" | "prizeRevealDeadlineDays">, now?: Date): Date | undefined;
@@ -1,10 +1,12 @@
1
1
  /**
2
- * maxPerUser gate + prize reveal deadline helper for the checkout pipeline
3
- * (SB6-C, SB8-A).
2
+ * maxPerUser gate for the checkout pipeline (SB6-C).
4
3
  *
5
4
  * `enforcePrizePoolCap` was removed in S-SBUNI-RULES (2026-05-13) — the
6
5
  * equivalent check now lives in `prizeDrawRule.preflightChecks` in the rule
7
- * registry and is invoked via `runSyncPreflight`.
6
+ * registry and is invoked via `runSyncPreflight`. `computePrizeRevealDeadline`
7
+ * (SB8-A) was removed with the buyer-claim-deadline mechanic — reveal is now
8
+ * fully automatic, there's nothing left for a buyer to "claim" before a
9
+ * deadline.
8
10
  */
9
11
  import { ValidationError } from "../../../../errors";
10
12
  import { orderRepository } from "../../../../repositories";
@@ -54,30 +56,3 @@ export async function enforceMaxPerUserForCart(args) {
54
56
  throw Object.assign(new ValidationError(`Purchase limit reached for one or more items. ${summary}`), { code: "MAX_PER_USER", violations });
55
57
  }
56
58
  }
57
- /**
58
- * SB8-A — compute the deadline by which the buyer must claim their prize
59
- * via the reveal API.
60
- *
61
- * Rules:
62
- * - If the reveal window has not yet opened:
63
- * deadline = min(revealWindowStart + revealDeadlineDays, revealWindowEnd)
64
- * - If the window is already open:
65
- * deadline = min(now + revealDeadlineDays, revealWindowEnd)
66
- *
67
- * Returns `undefined` if the product lacks the prize-draw fields.
68
- */
69
- export function computePrizeRevealDeadline(product, now = new Date()) {
70
- const windowStart = product.prizeRevealWindowStart
71
- ? new Date(product.prizeRevealWindowStart)
72
- : undefined;
73
- const windowEnd = product.prizeRevealWindowEnd
74
- ? new Date(product.prizeRevealWindowEnd)
75
- : undefined;
76
- if (!windowStart || !windowEnd)
77
- return undefined;
78
- const days = product.prizeRevealDeadlineDays ?? 3;
79
- const dayMs = 86400000;
80
- const base = windowStart.getTime() > now.getTime() ? windowStart : now;
81
- const candidate = new Date(base.getTime() + days * dayMs);
82
- return candidate.getTime() < windowEnd.getTime() ? candidate : windowEnd;
83
- }
@@ -13,12 +13,6 @@ export function orderDocumentToOrder(doc) {
13
13
  ...(item.prizeRevealStatus
14
14
  ? { prizeRevealStatus: item.prizeRevealStatus }
15
15
  : {}),
16
- ...(item.prizeRevealDeadline
17
- ? { prizeRevealDeadline: item.prizeRevealDeadline }
18
- : {}),
19
- ...(item.revealedItemNumber != null
20
- ? { revealedItemNumber: item.revealedItemNumber }
21
- : {}),
22
16
  ...(item.cancelledQuantity != null
23
17
  ? { cancelledQuantity: item.cancelledQuantity }
24
18
  : {}),
@@ -80,14 +74,7 @@ export function orderDocumentToOrder(doc) {
80
74
  },
81
75
  }
82
76
  : {}),
83
- ...(doc.prizeRevealDeadline
84
- ? {
85
- prizeRevealDeadline: doc.prizeRevealDeadline instanceof Date
86
- ? doc.prizeRevealDeadline.toISOString()
87
- : String(doc.prizeRevealDeadline),
88
- }
89
- : {}),
90
- ...(doc.prizeRevealExpired !== undefined ? { prizeRevealExpired: doc.prizeRevealExpired } : {}),
91
77
  ...(doc.prizeDrawProductId ? { prizeDrawProductId: doc.prizeDrawProductId } : {}),
78
+ ...(doc.prizeRevealMode ? { prizeRevealMode: doc.prizeRevealMode } : {}),
92
79
  };
93
80
  }
@@ -4,7 +4,7 @@ import { productRepository } from "../../../../repositories";
4
4
  import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
5
5
  import { isAdminUser } from "../../../../features/auth/role-predicates";
6
6
  import { productInputSchema, productUpdateSchema, auctionInputSchema, preOrderInputSchema, setFeaturedSchema, setStatusSchema } from "../../../shared/features/products/schema";
7
- import { assertProductOwnership, assertStatusTransition } from "./service";
7
+ import { assertProductOwnership, assertStatusTransition, assertPrizeDrawNotLocked, assertPrizeDrawWonItemsImmutable, } from "./service";
8
8
  import { ValidationError } from "../../../shared/errors/index";
9
9
  export async function createProductAction(input) {
10
10
  return wrapAction(async () => {
@@ -54,17 +54,19 @@ export async function createPreOrderAction(input) {
54
54
  export async function updateProductAction(productId, input) {
55
55
  return wrapAction(async () => {
56
56
  const user = await requireRoleUser(["seller", "admin"]);
57
- await assertProductOwnership(productId, user.uid);
57
+ const product = await assertProductOwnership(productId, user.uid);
58
58
  const parsed = productUpdateSchema.safeParse(input);
59
59
  if (!parsed.success)
60
60
  throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
61
+ assertPrizeDrawWonItemsImmutable(product, parsed.data.prizeDrawItems);
61
62
  return productRepository.update(productId, parsed.data);
62
63
  });
63
64
  }
64
65
  export async function deleteProductAction(productId) {
65
66
  return wrapAction(async () => {
66
67
  const user = await requireRoleUser(["seller", "admin"]);
67
- await assertProductOwnership(productId, user.uid);
68
+ const product = await assertProductOwnership(productId, user.uid);
69
+ assertPrizeDrawNotLocked(product, "deleted");
68
70
  return productRepository.delete(productId);
69
71
  });
70
72
  }
@@ -76,6 +78,9 @@ export async function setProductStatusAction(input) {
76
78
  throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
77
79
  const product = await assertProductOwnership(parsed.data.productId, user.uid);
78
80
  assertStatusTransition(product.status, parsed.data.status);
81
+ if (parsed.data.status !== "published") {
82
+ assertPrizeDrawNotLocked(product, "unpublished or archived");
83
+ }
79
84
  // SB-UNI-O — live listings must be admin-verified before a seller can publish them.
80
85
  if (parsed.data.status === "published" &&
81
86
  (product.listingType ?? "standard") === "live" &&
@@ -3,6 +3,21 @@ import type { ProductDocument } from "../../../shared/features/products/types";
3
3
  export declare function assertProductOwnership(productId: string, sellerId: string): Promise<ProductDocument>;
4
4
  /** Validate that a status transition is allowed. */
5
5
  export declare function assertStatusTransition(from: string, to: string): void;
6
+ /**
7
+ * Anti-scam guard — once a prize draw has at least one paid entry, the
8
+ * seller can no longer take it down (unpublish/archive/delete). Prevents a
9
+ * seller from dodging shipment of an expensive won prize by pulling the
10
+ * listing after the fact.
11
+ */
12
+ export declare function assertPrizeDrawNotLocked(product: Pick<ProductDocument, "listingType" | "prizeCurrentEntries">, action: string): void;
13
+ /**
14
+ * Anti-scam guard — once a prize item has been won, its title/images/
15
+ * description/condition/estimatedValue become immutable server-side (the
16
+ * client editor already locks this visually via PrizeDrawItemsEditor's
17
+ * `locked` state, but that's UI-only — this is the enforcement). Prevents a
18
+ * seller from swapping in a cheaper item after a buyer has already won.
19
+ */
20
+ export declare function assertPrizeDrawWonItemsImmutable(currentProduct: Pick<ProductDocument, "prizeDrawItems">, incomingItems: unknown): void;
6
21
  /** Assert the product has available stock for a purchase. */
7
22
  export declare function assertInStock(product: ProductDocument, quantity?: number): void;
8
23
  /** Derive effective price (respects currentBid for auctions, base price otherwise). */
@@ -1,6 +1,7 @@
1
1
  import { productRepository } from "../../../../repositories";
2
2
  import { isAuctionListing, isPreOrderListing, } from "../../../../features/products/utils/listing-type";
3
3
  import { ProductNotFoundError, ProductOwnershipError, ProductStatusError, ProductStockError, } from "../../../shared/features/products/errors";
4
+ import { ValidationError } from "../../../shared/errors/index";
4
5
  const ALLOWED_STATUS_TRANSITIONS = {
5
6
  draft: ["published", "archived"],
6
7
  published: ["archived", "draft"],
@@ -25,6 +26,46 @@ export function assertStatusTransition(from, to) {
25
26
  throw new ProductStatusError(from, to);
26
27
  }
27
28
  }
29
+ /**
30
+ * Anti-scam guard — once a prize draw has at least one paid entry, the
31
+ * seller can no longer take it down (unpublish/archive/delete). Prevents a
32
+ * seller from dodging shipment of an expensive won prize by pulling the
33
+ * listing after the fact.
34
+ */
35
+ export function assertPrizeDrawNotLocked(product, action) {
36
+ if ((product.listingType ?? "standard") !== "prize-draw")
37
+ return;
38
+ if ((product.prizeCurrentEntries ?? 0) <= 0)
39
+ return;
40
+ throw new ValidationError(`This prize draw has active entries and cannot be ${action} — buyers have already purchased tickets. The draw must run its course.`);
41
+ }
42
+ /**
43
+ * Anti-scam guard — once a prize item has been won, its title/images/
44
+ * description/condition/estimatedValue become immutable server-side (the
45
+ * client editor already locks this visually via PrizeDrawItemsEditor's
46
+ * `locked` state, but that's UI-only — this is the enforcement). Prevents a
47
+ * seller from swapping in a cheaper item after a buyer has already won.
48
+ */
49
+ export function assertPrizeDrawWonItemsImmutable(currentProduct, incomingItems) {
50
+ if (!Array.isArray(incomingItems))
51
+ return;
52
+ const current = currentProduct.prizeDrawItems ?? [];
53
+ for (const currentItem of current) {
54
+ if (!currentItem.isWon)
55
+ continue;
56
+ const incoming = incomingItems.find((it) => it.itemNumber === currentItem.itemNumber);
57
+ if (!incoming)
58
+ continue;
59
+ const changed = incoming.title !== currentItem.title ||
60
+ incoming.description !== currentItem.description ||
61
+ incoming.condition !== currentItem.condition ||
62
+ incoming.estimatedValue !== currentItem.estimatedValue ||
63
+ JSON.stringify(incoming.images) !== JSON.stringify(currentItem.images);
64
+ if (changed) {
65
+ throw new ValidationError(`Prize #${currentItem.itemNumber} has already been won and can no longer be edited.`);
66
+ }
67
+ }
68
+ }
28
69
  /** Assert the product has available stock for a purchase. */
29
70
  export function assertInStock(product, quantity = 1) {
30
71
  if ((product.availableQuantity ?? 0) < quantity) {
@@ -15,6 +15,8 @@ export declare const onProductStockChange: import("./types").DocumentTriggerFunc
15
15
  }, {
16
16
  [x: string]: import("../../..").JsonValue;
17
17
  }>;
18
+ export declare const onPrizeDrawPaymentConfirmed: import("./types").DocumentTriggerFunctionDefinition<import("../jobs/core/onPrizeDrawPaymentConfirmed").PrizeDrawOrderSnapshot, import("../jobs/core/onPrizeDrawPaymentConfirmed").PrizeDrawOrderSnapshot>;
19
+ export declare const prizeDrawSoldOutReveal: import("./types").DocumentTriggerFunctionDefinition<import("../jobs/core/prizeDrawSoldOutReveal").PrizeDrawStockSnapshot, import("../jobs/core/prizeDrawSoldOutReveal").PrizeDrawStockSnapshot>;
18
20
  export declare const onReviewWrite: import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>;
19
21
  export declare const onCategoryWrite: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").CategoryDoc, import("../jobs").CategoryDoc>;
20
22
  export declare const onStoreWrite: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").StoreDoc, import("../jobs").StoreDoc>;
@@ -53,7 +55,7 @@ export declare const FIRESTORE_TRIGGER_FUNCTIONS: readonly [import("./types").Do
53
55
  [x: string]: import("../../..").JsonValue;
54
56
  }, {
55
57
  [x: string]: import("../../..").JsonValue;
56
- }>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").CategoryDoc, import("../jobs").CategoryDoc>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").StoreDoc, import("../jobs").StoreDoc>, import("./types").DocumentTriggerFunctionDefinition<null, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<null, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<{
58
+ }>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs/core/onPrizeDrawPaymentConfirmed").PrizeDrawOrderSnapshot, import("../jobs/core/onPrizeDrawPaymentConfirmed").PrizeDrawOrderSnapshot>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs/core/prizeDrawSoldOutReveal").PrizeDrawStockSnapshot, import("../jobs/core/prizeDrawSoldOutReveal").PrizeDrawStockSnapshot>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").CategoryDoc, import("../jobs").CategoryDoc>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").StoreDoc, import("../jobs").StoreDoc>, import("./types").DocumentTriggerFunctionDefinition<null, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<null, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<{
57
59
  [x: string]: import("../../..").JsonValue;
58
60
  }, {
59
61
  [x: string]: import("../../..").JsonValue;
@@ -6,7 +6,7 @@
6
6
  * The path patterns mirror the original `functions/src/index.ts` declarations
7
7
  * to preserve identical deploy behavior.
8
8
  */
9
- import { onBidPlacedHandler, onCategoryWriteHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onProductStockChangeHandler, onProductWriteHandler, onReviewWriteHandler, onScamReportCreateHandler, onScamReportUpdateHandler, onStoreWriteHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, onShipmentItemWriteHandler, onShipmentLotWriteHandler, onShipmentHeaderWriteHandler, onShipmentDeletedHandler, onCatalogueSubmittedForApprovalHandler, onJobCreatedHandler, } from "../jobs/handlers";
9
+ import { onBidPlacedHandler, onCategoryWriteHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onProductStockChangeHandler, onProductWriteHandler, onReviewWriteHandler, onScamReportCreateHandler, onScamReportUpdateHandler, onStoreWriteHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, onShipmentItemWriteHandler, onShipmentLotWriteHandler, onShipmentHeaderWriteHandler, onShipmentDeletedHandler, onCatalogueSubmittedForApprovalHandler, onJobCreatedHandler, onPrizeDrawPaymentConfirmedHandler, prizeDrawSoldOutRevealHandler, } from "../jobs/handlers";
10
10
  import { defineFunction } from "./define";
11
11
  const REGION = "asia-south1";
12
12
  export const onBidPlaced = defineFunction({
@@ -44,6 +44,20 @@ export const onProductStockChange = defineFunction({
44
44
  handler: onProductStockChangeHandler,
45
45
  options: { region: REGION },
46
46
  });
47
+ export const onPrizeDrawPaymentConfirmed = defineFunction({
48
+ name: "onPrizeDrawPaymentConfirmed",
49
+ description: "Instant-mode prize-draw winner assignment the moment an order's payment is confirmed.",
50
+ trigger: { kind: "documentUpdated", pathPattern: "orders/{orderId}" },
51
+ handler: onPrizeDrawPaymentConfirmedHandler,
52
+ options: { region: REGION },
53
+ });
54
+ export const prizeDrawSoldOutReveal = defineFunction({
55
+ name: "prizeDrawSoldOutReveal",
56
+ description: "Assign winners for all outstanding prize-draw orders the moment a scheduled-mode draw sells out.",
57
+ trigger: { kind: "documentWritten", pathPattern: "products/{productId}" },
58
+ handler: prizeDrawSoldOutRevealHandler,
59
+ options: { region: REGION },
60
+ });
47
61
  export const onReviewWrite = defineFunction({
48
62
  name: "onReviewWrite",
49
63
  description: "Recompute product + store rating aggregates on review writes.",
@@ -155,6 +169,8 @@ export const FIRESTORE_TRIGGER_FUNCTIONS = [
155
169
  onOrderStatusChange,
156
170
  onProductWrite,
157
171
  onProductStockChange,
172
+ onPrizeDrawPaymentConfirmed,
173
+ prizeDrawSoldOutReveal,
158
174
  onReviewWrite,
159
175
  onCategoryWrite,
160
176
  onStoreWrite,
@@ -6,6 +6,7 @@ export declare const paymentReviewAutoApprove: import("./types").ScheduledFuncti
6
6
  export declare const couponExpiry: import("./types").ScheduledFunctionDefinition;
7
7
  export declare const offerExpiry: import("./types").ScheduledFunctionDefinition;
8
8
  export declare const productStatsSync: import("./types").ScheduledFunctionDefinition;
9
+ export declare const revenueRollup: import("./types").ScheduledFunctionDefinition;
9
10
  export declare const dailyDataCleanup: import("./types").ScheduledFunctionDefinition;
10
11
  export declare const countersReconcile: import("./types").ScheduledFunctionDefinition;
11
12
  export declare const positionsReconcile: import("./types").ScheduledFunctionDefinition;
@@ -18,11 +19,8 @@ export declare const cleanupRtdbEvents: import("./types").ScheduledFunctionDefin
18
19
  export declare const mediaTmpCleanup: import("./types").ScheduledFunctionDefinition;
19
20
  export declare const draftPrune: import("./types").ScheduledFunctionDefinition;
20
21
  export declare const testerSandboxCleanup: import("./types").ScheduledFunctionDefinition;
21
- export declare const prizeRevealOpen: import("./types").ScheduledFunctionDefinition;
22
- export declare const prizeRevealClose: import("./types").ScheduledFunctionDefinition;
23
- export declare const prizeRevealExpiry: import("./types").ScheduledFunctionDefinition;
24
- export declare const prizeRevealReminder: import("./types").ScheduledFunctionDefinition;
22
+ export declare const prizeDrawExpiryReveal: import("./types").ScheduledFunctionDefinition;
25
23
  export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
26
24
  export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
27
25
  export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
28
- export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
26
+ export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
@@ -1,6 +1,6 @@
1
1
  // Scheduled function definitions (Cloud Scheduler triggers).
2
2
  // Cron syntax accepts both Firebase shorthand and standard cron expressions.
3
- import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeRevealCloseHandler, prizeRevealExpiryHandler, prizeRevealOpenHandler, prizeRevealReminderHandler, productStatsSyncHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, } from "../jobs/handlers";
3
+ import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, } from "../jobs/handlers";
4
4
  import { defineFunction } from "./define";
5
5
  const REGION = "asia-south1";
6
6
  const EVERY_5_MIN = "every 5 minutes";
@@ -60,6 +60,13 @@ export const productStatsSync = defineFunction({
60
60
  handler: productStatsSyncHandler,
61
61
  options: { region: REGION, timeoutSeconds: 540, memory: "256MiB", maxInstances: 1 },
62
62
  });
63
+ export const revenueRollup = defineFunction({
64
+ name: "revenueRollup",
65
+ description: "Pre-compute total delivered-order revenue into analytics/dashboardRollup, replacing an unbounded per-request scan (daily 01:30 UTC).",
66
+ trigger: { kind: "schedule", cron: "30 1 * * *", timeZone: "UTC" },
67
+ handler: revenueRollupHandler,
68
+ options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
69
+ });
63
70
  export const dailyDataCleanup = defineFunction({
64
71
  name: "dailyDataCleanup",
65
72
  description: "Daily data cleanup (drafts, transient records) at 02:00 UTC.",
@@ -144,32 +151,11 @@ export const testerSandboxCleanup = defineFunction({
144
151
  handler: testerSandboxCleanupHandler,
145
152
  options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
146
153
  });
147
- export const prizeRevealOpen = defineFunction({
148
- name: "prizeRevealOpen",
149
- description: "Flip prize-draw reveals pending→open and send opening notifications.",
150
- trigger: { kind: "schedule", cron: EVERY_5_MIN },
151
- handler: prizeRevealOpenHandler,
152
- options: { region: REGION, timeoutSeconds: 120, memory: "256MiB", maxInstances: 1 },
153
- });
154
- export const prizeRevealClose = defineFunction({
155
- name: "prizeRevealClose",
156
- description: "Flip prize-draw reveals open→closed.",
154
+ export const prizeDrawExpiryReveal = defineFunction({
155
+ name: "prizeDrawExpiryReveal",
156
+ description: "Assign winners for outstanding paid orders on scheduled-mode prize draws past their expiry, then close them (every 5 minutes).",
157
157
  trigger: { kind: "schedule", cron: EVERY_5_MIN },
158
- handler: prizeRevealCloseHandler,
159
- options: { region: REGION, timeoutSeconds: 60, memory: "256MiB", maxInstances: 1 },
160
- });
161
- export const prizeRevealExpiry = defineFunction({
162
- name: "prizeRevealExpiry",
163
- description: "Auto-refund unrevealed prize-draw entries past deadline (every 6 hours UTC).",
164
- trigger: { kind: "schedule", cron: "0 */6 * * *", timeZone: "UTC" },
165
- handler: prizeRevealExpiryHandler,
166
- options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
167
- });
168
- export const prizeRevealReminder = defineFunction({
169
- name: "prizeRevealReminder",
170
- description: "Nudge prize-draw buyers <24h to deadline (daily 10:00 IST).",
171
- trigger: { kind: "schedule", cron: "0 10 * * *", timeZone: "Asia/Kolkata" },
172
- handler: prizeRevealReminderHandler,
158
+ handler: prizeDrawExpiryRevealHandler,
173
159
  options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
174
160
  });
175
161
  export const bundleStockSync = defineFunction({
@@ -202,6 +188,7 @@ export const SCHEDULED_FUNCTIONS = [
202
188
  couponExpiry,
203
189
  offerExpiry,
204
190
  productStatsSync,
191
+ revenueRollup,
205
192
  dailyDataCleanup,
206
193
  countersReconcile,
207
194
  positionsReconcile,
@@ -214,10 +201,7 @@ export const SCHEDULED_FUNCTIONS = [
214
201
  mediaTmpCleanup,
215
202
  draftPrune,
216
203
  testerSandboxCleanup,
217
- prizeRevealOpen,
218
- prizeRevealClose,
219
- prizeRevealExpiry,
220
- prizeRevealReminder,
204
+ prizeDrawExpiryReveal,
221
205
  bundleStockSync,
222
206
  emiInstallmentReminder,
223
207
  catalogueImageStalenessReminder,
@@ -16,9 +16,12 @@ function isMemberAvailable(data) {
16
16
  return false;
17
17
  return data.status === PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED;
18
18
  }
19
- async function computeBundleStockStatus(productIds, ctx) {
19
+ async function sweepBundleMembers(productIds, ctx) {
20
20
  if (productIds.length === 0)
21
- return "out_of_stock";
21
+ return { stockStatus: "out_of_stock", originalTotal: undefined };
22
+ let stockStatus = "in_stock";
23
+ let originalTotal = 0;
24
+ let resolvedCount = 0;
22
25
  for (let i = 0; i < productIds.length; i += 30) {
23
26
  const chunk = productIds.slice(i, i + 30);
24
27
  const snap = await ctx.db
@@ -26,14 +29,26 @@ async function computeBundleStockStatus(productIds, ctx) {
26
29
  .where("__name__", "in", chunk)
27
30
  .get();
28
31
  if (snap.size < chunk.length)
29
- return "out_of_stock";
32
+ stockStatus = "out_of_stock";
30
33
  for (const doc of snap.docs) {
31
34
  const data = doc.data();
32
35
  if (!isMemberAvailable(data))
33
- return "out_of_stock";
36
+ stockStatus = "out_of_stock";
37
+ if (typeof data.price === "number" && originalTotal !== undefined) {
38
+ originalTotal += data.price;
39
+ resolvedCount++;
40
+ }
41
+ else {
42
+ originalTotal = undefined;
43
+ }
34
44
  }
35
45
  }
36
- return "in_stock";
46
+ if (resolvedCount !== productIds.length)
47
+ originalTotal = undefined;
48
+ return {
49
+ stockStatus,
50
+ originalTotal: originalTotal !== undefined ? Math.round(originalTotal * 100) / 100 : undefined,
51
+ };
37
52
  }
38
53
  export async function runBundleStockSync(ctx) {
39
54
  if (ctx.env("FEATURE_BUNDLES") !== "true") {
@@ -57,10 +72,13 @@ export async function runBundleStockSync(ctx) {
57
72
  const productIds = data.bundleProductIds ?? [];
58
73
  if (productIds.length === 0)
59
74
  continue;
60
- const nextStatus = await computeBundleStockStatus(productIds, ctx);
61
- if (nextStatus !== data.bundleStockStatus) {
75
+ const { stockStatus: nextStatus, originalTotal: nextOriginalTotal } = await sweepBundleMembers(productIds, ctx);
76
+ const statusChanged = nextStatus !== data.bundleStockStatus;
77
+ const totalChanged = nextOriginalTotal !== data.bundleOriginalTotal;
78
+ if (statusChanged || totalChanged) {
62
79
  await bundleDoc.ref.update({
63
80
  [CATEGORY_FIELDS.BUNDLE_STOCK_STATUS]: nextStatus,
81
+ [CATEGORY_FIELDS.BUNDLE_ORIGINAL_TOTAL]: nextOriginalTotal ?? null,
64
82
  [CATEGORY_FIELDS.BUNDLE_QUERY_RESOLVED_AT]: ctx.now,
65
83
  [COMMON_FIELDS.UPDATED_AT]: ctx.now,
66
84
  });
@@ -15,7 +15,13 @@ async function reconcileCategories(ctx) {
15
15
  const leafCounts = {};
16
16
  for (const doc of snap.docs) {
17
17
  const data = doc.data();
18
- const catId = data.category;
18
+ // `category` is @deprecated in favor of `categorySlugs[]` — mirrors the
19
+ // same effective-category selection as onProductWrite.ts's live trigger
20
+ // so the nightly reconcile agrees with real-time updates instead of
21
+ // silently no-op'ing on products written via categorySlugs-only paths.
22
+ const catId = Array.isArray(data.categorySlugs) && data.categorySlugs.length > 0
23
+ ? data.categorySlugs[0]
24
+ : data.category;
19
25
  if (!catId)
20
26
  continue;
21
27
  if (!leafCounts[catId])
@@ -26,10 +26,10 @@ export { runWeeklyPayoutEligibility, type WeeklyPayoutEligibilityResult, } from
26
26
  export { runHardBanCascade } from "./hardBanCascade";
27
27
  export { JOB_RUNNERS, type JobRunner, type JobRunResult, } from "./jobRunners";
28
28
  export { runAuctionSettlement } from "./auctionSettlement";
29
- export { runPrizeRevealOpen } from "./prizeRevealOpen";
30
- export { runPrizeRevealClose } from "./prizeRevealClose";
31
- export { runPrizeRevealExpiry } from "./prizeRevealExpiry";
32
- export { runPrizeRevealReminder } from "./prizeRevealReminder";
29
+ export { assignPrizeDrawWinner, type AssignPrizeDrawWinnerResult } from "./prizeDrawAssignWinner";
30
+ export { handlePrizeDrawPaymentConfirmed } from "./onPrizeDrawPaymentConfirmed";
31
+ export { handlePrizeDrawSoldOut } from "./prizeDrawSoldOutReveal";
32
+ export { runPrizeDrawExpiryReveal } from "./prizeDrawExpiryReveal";
33
33
  export { runPromotions, type PromotionsCallableResult } from "./promotions";
34
34
  export { runListingProcessor, supportedListingCollections, type ListingRequestBody, type ListingResponseBody, } from "./listingProcessor";
35
35
  export { runStoreAnalytics, type StoreAnalyticsInput, type StoreAnalyticsResult, } from "./storeAnalytics";
@@ -27,10 +27,10 @@ export { runWeeklyPayoutEligibility, } from "./weeklyPayoutEligibility";
27
27
  export { runHardBanCascade } from "./hardBanCascade";
28
28
  export { JOB_RUNNERS, } from "./jobRunners";
29
29
  export { runAuctionSettlement } from "./auctionSettlement";
30
- export { runPrizeRevealOpen } from "./prizeRevealOpen";
31
- export { runPrizeRevealClose } from "./prizeRevealClose";
32
- export { runPrizeRevealExpiry } from "./prizeRevealExpiry";
33
- export { runPrizeRevealReminder } from "./prizeRevealReminder";
30
+ export { assignPrizeDrawWinner } from "./prizeDrawAssignWinner";
31
+ export { handlePrizeDrawPaymentConfirmed } from "./onPrizeDrawPaymentConfirmed";
32
+ export { handlePrizeDrawSoldOut } from "./prizeDrawSoldOutReveal";
33
+ export { runPrizeDrawExpiryReveal } from "./prizeDrawExpiryReveal";
34
34
  // Callables
35
35
  export { runPromotions } from "./promotions";
36
36
  export { runListingProcessor, supportedListingCollections, } from "./listingProcessor";
@@ -11,6 +11,8 @@
11
11
  import { runWeeklyPayoutEligibility } from "./weeklyPayoutEligibility";
12
12
  import { runHardBanCascade } from "./hardBanCascade";
13
13
  import { runResetOtpVerification } from "./resetOtpVerification";
14
+ import { runWhatsAppNotify } from "./whatsappNotify";
15
+ import { runNewsletterExport } from "./newsletterExport";
14
16
  async function runPayoutsWeeklyJob(_payload, ctx) {
15
17
  const { payoutsCreated, ordersProcessed, payoutIds } = await runWeeklyPayoutEligibility(ctx);
16
18
  return {
@@ -37,8 +39,16 @@ async function runHardBanCascadeJob(payload, ctx) {
37
39
  async function runResetOtpVerificationJob(_payload, ctx) {
38
40
  return runResetOtpVerification(ctx);
39
41
  }
42
+ async function runWhatsAppNotifyJob(payload, ctx) {
43
+ return runWhatsAppNotify(payload, ctx);
44
+ }
45
+ async function runNewsletterExportJob(_payload, ctx) {
46
+ return runNewsletterExport(ctx);
47
+ }
40
48
  export const JOB_RUNNERS = {
41
49
  payoutsWeekly: runPayoutsWeeklyJob,
42
50
  hardBanCascade: runHardBanCascadeJob,
43
51
  resetOtpVerification: runResetOtpVerificationJob,
52
+ whatsappNotify: runWhatsAppNotifyJob,
53
+ newsletterExport: runNewsletterExportJob,
44
54
  };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Core: async newsletter-subscriber CSV export (`newsletterExport` job).
3
+ *
4
+ * `GET /api/admin/newsletter/export` used to build the full CSV in-process,
5
+ * synchronously, for up to 10,000 subscribers — a real 10s-timeout risk on
6
+ * Vercel Hobby as the list grows (CLAUDE.md Rule #6). This runner does the
7
+ * same work inside a Firebase Function instead and writes the CSV text onto
8
+ * the job's `result.data.csv` field — small enough for a Firestore doc at
9
+ * realistic subscriber counts, and avoids routing through Storage (which
10
+ * would need a `/media/...` proxy URL per the Media Architecture rules
11
+ * rather than a raw signed URL).
12
+ */
13
+ import type { JobContext } from "../runtime/types";
14
+ import type { JobRunResult } from "./jobRunners";
15
+ export declare function runNewsletterExport(ctx: JobContext): Promise<JobRunResult>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Core: async newsletter-subscriber CSV export (`newsletterExport` job).
3
+ *
4
+ * `GET /api/admin/newsletter/export` used to build the full CSV in-process,
5
+ * synchronously, for up to 10,000 subscribers — a real 10s-timeout risk on
6
+ * Vercel Hobby as the list grows (CLAUDE.md Rule #6). This runner does the
7
+ * same work inside a Firebase Function instead and writes the CSV text onto
8
+ * the job's `result.data.csv` field — small enough for a Firestore doc at
9
+ * realistic subscriber counts, and avoids routing through Storage (which
10
+ * would need a `/media/...` proxy URL per the Media Architecture rules
11
+ * rather than a raw signed URL).
12
+ */
13
+ import { newsletterRepository } from "../../../../repositories";
14
+ import { sortBy } from "../../../../constants/sort";
15
+ import { COMMON_FIELDS } from "../../../../constants/field-names";
16
+ function escape(value) {
17
+ const s = String(value ?? "");
18
+ return s.includes(",") || s.includes('"') || s.includes("\n")
19
+ ? `"${s.replace(/"/g, '""')}"`
20
+ : s;
21
+ }
22
+ function csvRow(cols) {
23
+ return cols.map(escape).join(",");
24
+ }
25
+ export async function runNewsletterExport(ctx) {
26
+ const result = await newsletterRepository.list({
27
+ sorts: sortBy(COMMON_FIELDS.CREATED_AT),
28
+ page: "1",
29
+ pageSize: "10000",
30
+ });
31
+ const rows = result.data;
32
+ const header = csvRow(["id", "email", "status", "source", "subscribedAt", "createdAt"]);
33
+ const dataRows = rows.map((s) => csvRow([s["id"], s["email"] ?? "", s["status"] ?? "", s["source"] ?? "", s["subscribedAt"] ?? "", s["createdAt"] ?? ""]));
34
+ const csv = [header, ...dataRows].join("\r\n");
35
+ ctx.logger.info("newsletterExport: complete", { subscriberCount: rows.length });
36
+ return {
37
+ summary: { total: rows.length, succeeded: rows.length, skipped: 0, failed: 0 },
38
+ succeeded: [],
39
+ skipped: [],
40
+ failed: [],
41
+ data: { csv, subscriberCount: rows.length },
42
+ };
43
+ }
@@ -6,6 +6,8 @@ export type OrderAfter = {
6
6
  userEmail: string;
7
7
  productTitle: string;
8
8
  trackingNumber?: string;
9
+ /** Buyer opted into the ₹10 WhatsApp order-updates addon at checkout. */
10
+ whatsappNotifyAddon?: boolean;
9
11
  };
10
12
  export type OrderBefore = {
11
13
  status: OrderStatus;
@@ -56,6 +56,7 @@ export async function handleOrderStatusChange(input, ctx) {
56
56
  relatedId: orderId,
57
57
  relatedType: "order",
58
58
  userEmail,
59
+ orderWhatsappAddonPaid: after.whatsappNotifyAddon === true,
59
60
  });
60
61
  try {
61
62
  await getAdminRealtimeDb().ref(`notifications/${after.userId}`).push({