@mohasinac/appkit 4.2.0 → 4.3.1

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 (258) hide show
  1. package/dist/_internal/client/features/layout/navActive.d.ts +17 -0
  2. package/dist/_internal/client/features/layout/navActive.js +33 -0
  3. package/dist/_internal/server/features/auth/actions.d.ts +0 -1
  4. package/dist/_internal/server/features/auth/actions.js +0 -1
  5. package/dist/_internal/server/features/bundles/data.d.ts +22 -4
  6. package/dist/_internal/server/features/bundles/data.js +41 -4
  7. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  8. package/dist/_internal/server/features/bundles/index.js +1 -1
  9. package/dist/_internal/server/features/checkout/actions.js +7 -78
  10. package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
  11. package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
  12. package/dist/_internal/server/features/orders/adapters.js +1 -14
  13. package/dist/_internal/server/features/products/actions.js +8 -3
  14. package/dist/_internal/server/features/products/service.d.ts +15 -0
  15. package/dist/_internal/server/features/products/service.js +41 -0
  16. package/dist/_internal/server/functions/firestore.d.ts +3 -1
  17. package/dist/_internal/server/functions/firestore.js +17 -1
  18. package/dist/_internal/server/functions/scheduled.d.ts +2 -5
  19. package/dist/_internal/server/functions/scheduled.js +6 -30
  20. package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
  21. package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
  22. package/dist/_internal/server/jobs/core/index.d.ts +4 -4
  23. package/dist/_internal/server/jobs/core/index.js +4 -4
  24. package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
  25. package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
  26. package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
  27. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +2 -2
  28. package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
  29. package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
  30. package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
  31. package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
  32. package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
  33. package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
  34. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -4
  35. package/dist/_internal/server/jobs/handlers/index.js +8 -6
  36. package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
  37. package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
  38. package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
  39. package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
  40. package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
  41. package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
  42. package/dist/_internal/shared/actions/action-registry.js +40 -10
  43. package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
  44. package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
  45. package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
  46. package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
  47. package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
  48. package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
  49. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  50. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  51. package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
  52. package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
  53. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
  54. package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
  55. package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
  56. package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
  57. package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
  58. package/dist/constants/api-endpoints.d.ts +6 -9
  59. package/dist/constants/api-endpoints.js +5 -5
  60. package/dist/constants/field-names.d.ts +17 -1
  61. package/dist/constants/field-names.js +17 -1
  62. package/dist/features/account/components/UserSidebar.js +21 -9
  63. package/dist/features/account/hooks/useNotifications.js +5 -0
  64. package/dist/features/admin/actions/admin-actions.js +6 -0
  65. package/dist/features/admin/actions/notification-actions.js +1 -2
  66. package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
  67. package/dist/features/admin/components/AdminGuideHubView.js +15 -1
  68. package/dist/features/admin/components/AdminPaymentsGuideView.d.ts +2 -0
  69. package/dist/features/admin/components/AdminPaymentsGuideView.js +36 -0
  70. package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
  71. package/dist/features/admin/components/AdminSidebar.js +18 -9
  72. package/dist/features/admin/components/AdminSiteSettingsView.js +33 -6
  73. package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
  74. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +2 -0
  75. package/dist/features/admin/components/AdminWhatsAppGuideView.js +35 -0
  76. package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
  77. package/dist/features/admin/hooks/useAdminListing.js +4 -2
  78. package/dist/features/admin/schemas/firestore.d.ts +13 -5
  79. package/dist/features/admin/schemas/firestore.js +1 -2
  80. package/dist/features/auctions/actions/bid-actions.js +3 -1
  81. package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
  82. package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
  83. package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
  84. package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
  85. package/dist/features/auctions/components/CollapsibleBidHistory.js +22 -3
  86. package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
  87. package/dist/features/auctions/components/LiveBidPrice.js +17 -0
  88. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
  89. package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
  90. package/dist/features/auctions/hooks/useBids.d.ts +33 -0
  91. package/dist/features/auctions/hooks/useBids.js +32 -0
  92. package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
  93. package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
  94. package/dist/features/auth/checkout-value-otp.d.ts +7 -10
  95. package/dist/features/auth/checkout-value-otp.js +14 -10
  96. package/dist/features/auth/server.d.ts +0 -1
  97. package/dist/features/auth/server.js +0 -1
  98. package/dist/features/blog/api/[slug]/route.d.ts +5 -0
  99. package/dist/features/blog/api/[slug]/route.js +12 -3
  100. package/dist/features/blog/components/BlogPostView.d.ts +2 -0
  101. package/dist/features/blog/components/BlogPostView.js +5 -4
  102. package/dist/features/blog/hooks/useBlog.d.ts +2 -0
  103. package/dist/features/blog/hooks/useBlog.js +2 -0
  104. package/dist/features/blog/repository/blog.repository.d.ts +7 -0
  105. package/dist/features/blog/repository/blog.repository.js +44 -0
  106. package/dist/features/blog/schemas/firestore.d.ts +3 -0
  107. package/dist/features/blog/schemas/firestore.js +1 -0
  108. package/dist/features/blog/types/index.d.ts +2 -0
  109. package/dist/features/cart/components/index.d.ts +0 -2
  110. package/dist/features/cart/components/index.js +0 -1
  111. package/dist/features/cart/hooks/useCartCount.js +22 -5
  112. package/dist/features/cart/hooks/useGuestCart.js +11 -1
  113. package/dist/features/cart/utils/guest-cart.d.ts +4 -0
  114. package/dist/features/cart/utils/guest-cart.js +10 -0
  115. package/dist/features/cart/utils/pending-ops.d.ts +17 -0
  116. package/dist/features/cart/utils/pending-ops.js +46 -5
  117. package/dist/features/categories/components/BrandDetailPageView.js +6 -2
  118. package/dist/features/categories/components/BundleDetailView.js +4 -2
  119. package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
  120. package/dist/features/categories/schemas/firestore.d.ts +7 -0
  121. package/dist/features/checkout/actions/index.d.ts +0 -1
  122. package/dist/features/checkout/actions/index.js +0 -1
  123. package/dist/features/checkout/schemas/firestore.d.ts +2 -2
  124. package/dist/features/events/actions/event-actions.d.ts +2 -0
  125. package/dist/features/events/actions/event-actions.js +5 -0
  126. package/dist/features/events/components/EventCard.js +2 -3
  127. package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
  128. package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
  129. package/dist/features/events/components/index.d.ts +2 -0
  130. package/dist/features/events/components/index.js +1 -0
  131. package/dist/features/events/repository/events.repository.d.ts +6 -0
  132. package/dist/features/events/repository/events.repository.js +21 -0
  133. package/dist/features/history/repository/user-history.repository.d.ts +1 -1
  134. package/dist/features/history/utils/guest-history.d.ts +1 -1
  135. package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
  136. package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
  137. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  138. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  139. package/dist/features/layout/AppLayoutShell.js +44 -10
  140. package/dist/features/layout/NavbarLayout.js +1 -1
  141. package/dist/features/layout/TitleBarLayout.js +2 -2
  142. package/dist/features/media/MediaVideo.d.ts +7 -0
  143. package/dist/features/media/MediaVideo.js +36 -5
  144. package/dist/features/orders/components/OrdersList.js +1 -5
  145. package/dist/features/orders/schemas/firestore.d.ts +2 -8
  146. package/dist/features/orders/schemas/index.d.ts +3 -32
  147. package/dist/features/orders/schemas/index.js +1 -4
  148. package/dist/features/orders/types/index.d.ts +6 -9
  149. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  150. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
  151. package/dist/features/products/actions/product-actions.js +13 -10
  152. package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
  153. package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
  154. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  155. package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
  156. package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
  157. package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
  158. package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
  159. package/dist/features/products/components/PrizeRevealModal.js +18 -147
  160. package/dist/features/products/components/ProductDetailPageView.js +72 -30
  161. package/dist/features/products/components/ProductForm.js +7 -14
  162. package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
  163. package/dist/features/products/components/ProductGalleryClient.js +29 -10
  164. package/dist/features/products/components/ProductsIndexListing.js +2 -1
  165. package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
  166. package/dist/features/products/components/ShowGroupSection.js +2 -2
  167. package/dist/features/products/components/SublistingCarouselSection.js +1 -1
  168. package/dist/features/products/components/index.d.ts +4 -2
  169. package/dist/features/products/components/index.js +1 -0
  170. package/dist/features/products/constants/action-defs.d.ts +1 -3
  171. package/dist/features/products/constants/action-defs.js +0 -5
  172. package/dist/features/products/repository/products.repository.d.ts +6 -0
  173. package/dist/features/products/repository/products.repository.js +20 -1
  174. package/dist/features/products/schemas/firestore.d.ts +28 -3
  175. package/dist/features/products/schemas/index.d.ts +11 -3
  176. package/dist/features/products/schemas/index.js +10 -2
  177. package/dist/features/products/types/index.d.ts +2 -1
  178. package/dist/features/reviews/actions/review-actions.js +1 -0
  179. package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
  180. package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
  181. package/dist/features/reviews/components/ReviewsList.js +2 -1
  182. package/dist/features/reviews/schemas/firestore.d.ts +3 -0
  183. package/dist/features/reviews/schemas/index.d.ts +6 -0
  184. package/dist/features/reviews/schemas/index.js +2 -0
  185. package/dist/features/reviews/types/index.d.ts +3 -0
  186. package/dist/features/scams/actions/scam-actions.d.ts +3 -0
  187. package/dist/features/scams/actions/scam-actions.js +3 -2
  188. package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
  189. package/dist/features/scams/components/ScamProfileView.js +2 -2
  190. package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
  191. package/dist/features/scams/repository/scammer.repository.js +24 -0
  192. package/dist/features/seller/actions/seller-actions.js +8 -0
  193. package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
  194. package/dist/features/seller/components/SellerSidebar.js +20 -9
  195. package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
  196. package/dist/features/stores/components/StoreGuideHubView.js +7 -1
  197. package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
  198. package/dist/features/stores/components/StoreNavTabs.js +22 -9
  199. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +3 -0
  200. package/dist/features/stores/components/StoreWhatsAppGuideView.js +12 -0
  201. package/dist/features/stores/components/index.d.ts +2 -0
  202. package/dist/features/stores/components/index.js +1 -0
  203. package/dist/features/tester/actions/index.d.ts +1 -0
  204. package/dist/features/tester/actions/index.js +1 -0
  205. package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
  206. package/dist/features/tester/actions/leaderboard-actions.js +5 -0
  207. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
  208. package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
  209. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
  210. package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
  211. package/dist/features/tester/components/index.d.ts +2 -0
  212. package/dist/features/tester/components/index.js +1 -0
  213. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
  214. package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
  215. package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
  216. package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
  217. package/dist/features/tester/schemas/firestore.d.ts +22 -1
  218. package/dist/features/tester/schemas/firestore.js +8 -0
  219. package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
  220. package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
  221. package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
  222. package/dist/features/tester/seed-data/products-tester-seed-data.js +154 -9
  223. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +252 -15
  224. package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
  225. package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
  226. package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
  227. package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
  228. package/dist/index.d.ts +23 -22
  229. package/dist/index.js +31 -58
  230. package/dist/jobs.d.ts +1 -1
  231. package/dist/jobs.js +2 -2
  232. package/dist/next/routing/route-map.d.ts +9 -2
  233. package/dist/next/routing/route-map.js +4 -1
  234. package/dist/schemas/registry.d.ts +6 -26
  235. package/dist/seed/bids-seed-data.js +14 -2
  236. package/dist/seed/blog-posts-seed-data.js +2 -1
  237. package/dist/seed/categories-seed-data.js +11 -2
  238. package/dist/seed/events-seed-data.js +11 -11
  239. package/dist/seed/faq-seed-data.js +5 -1
  240. package/dist/seed/products-auctions-seed-data.js +2 -2
  241. package/dist/seed/products-preorders-seed-data.d.ts +2 -1
  242. package/dist/seed/products-prize-draws-seed-data.js +56 -10
  243. package/dist/seed/products-standard-seed-data.js +80 -3
  244. package/dist/seed/scammers-seed-data.js +75 -2
  245. package/dist/seed/site-settings-seed-data.js +5 -2
  246. package/dist/server-entry.d.ts +0 -1
  247. package/dist/server-entry.js +0 -1
  248. package/dist/server.d.ts +4 -20
  249. package/dist/server.js +10 -57
  250. package/dist/styles.css +1 -277
  251. package/dist/tailwind-utilities.css +1 -1
  252. package/dist/ui/components/BaseListingCard.js +1 -1
  253. package/dist/ui/components/ImageLightbox.d.ts +9 -0
  254. package/dist/ui/components/ImageLightbox.js +6 -3
  255. package/dist/ui/components/ListingToolbar.d.ts +18 -1
  256. package/dist/ui/components/ListingToolbar.js +5 -2
  257. package/dist/ui/components/StickyToolbar.js +13 -8
  258. package/package.json +1 -1
@@ -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,
@@ -19,11 +19,8 @@ export declare const cleanupRtdbEvents: import("./types").ScheduledFunctionDefin
19
19
  export declare const mediaTmpCleanup: import("./types").ScheduledFunctionDefinition;
20
20
  export declare const draftPrune: import("./types").ScheduledFunctionDefinition;
21
21
  export declare const testerSandboxCleanup: import("./types").ScheduledFunctionDefinition;
22
- export declare const prizeRevealOpen: import("./types").ScheduledFunctionDefinition;
23
- export declare const prizeRevealClose: import("./types").ScheduledFunctionDefinition;
24
- export declare const prizeRevealExpiry: import("./types").ScheduledFunctionDefinition;
25
- export declare const prizeRevealReminder: import("./types").ScheduledFunctionDefinition;
22
+ export declare const prizeDrawExpiryReveal: import("./types").ScheduledFunctionDefinition;
26
23
  export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
27
24
  export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
28
25
  export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
29
- 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, 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, revenueRollupHandler, 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";
@@ -151,32 +151,11 @@ export const testerSandboxCleanup = defineFunction({
151
151
  handler: testerSandboxCleanupHandler,
152
152
  options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
153
153
  });
154
- export const prizeRevealOpen = defineFunction({
155
- name: "prizeRevealOpen",
156
- description: "Flip prize-draw reveals pending→open and send opening notifications.",
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: prizeRevealOpenHandler,
159
- options: { region: REGION, timeoutSeconds: 120, memory: "256MiB", maxInstances: 1 },
160
- });
161
- export const prizeRevealClose = defineFunction({
162
- name: "prizeRevealClose",
163
- description: "Flip prize-draw reveals open→closed.",
164
- trigger: { kind: "schedule", cron: EVERY_5_MIN },
165
- handler: prizeRevealCloseHandler,
166
- options: { region: REGION, timeoutSeconds: 60, memory: "256MiB", maxInstances: 1 },
167
- });
168
- export const prizeRevealExpiry = defineFunction({
169
- name: "prizeRevealExpiry",
170
- description: "Auto-refund unrevealed prize-draw entries past deadline (every 6 hours UTC).",
171
- trigger: { kind: "schedule", cron: "0 */6 * * *", timeZone: "UTC" },
172
- handler: prizeRevealExpiryHandler,
173
- options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
174
- });
175
- export const prizeRevealReminder = defineFunction({
176
- name: "prizeRevealReminder",
177
- description: "Nudge prize-draw buyers <24h to deadline (daily 10:00 IST).",
178
- trigger: { kind: "schedule", cron: "0 10 * * *", timeZone: "Asia/Kolkata" },
179
- handler: prizeRevealReminderHandler,
158
+ handler: prizeDrawExpiryRevealHandler,
180
159
  options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
181
160
  });
182
161
  export const bundleStockSync = defineFunction({
@@ -222,10 +201,7 @@ export const SCHEDULED_FUNCTIONS = [
222
201
  mediaTmpCleanup,
223
202
  draftPrune,
224
203
  testerSandboxCleanup,
225
- prizeRevealOpen,
226
- prizeRevealClose,
227
- prizeRevealExpiry,
228
- prizeRevealReminder,
204
+ prizeDrawExpiryReveal,
229
205
  bundleStockSync,
230
206
  emiInstallmentReminder,
231
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";
@@ -0,0 +1,19 @@
1
+ import type { JobContext } from "../runtime/types";
2
+ export interface PrizeDrawOrderSnapshot {
3
+ paymentStatus?: string;
4
+ prizeDrawProductId?: string;
5
+ items?: Array<{
6
+ listingType?: string;
7
+ }>;
8
+ }
9
+ /**
10
+ * Instant-mode prize-draw winner assignment. Fires when an order's
11
+ * paymentStatus transitions into "paid" — not at order creation, since a
12
+ * prize-draw order isn't guaranteed to be paid yet at that point (manual
13
+ * bank/UPI payments are confirmed later by the seller/admin; Razorpay
14
+ * confirms near-instantly but still goes through this same status flip).
15
+ */
16
+ export declare function handlePrizeDrawPaymentConfirmed(input: {
17
+ orderId: string;
18
+ order: PrizeDrawOrderSnapshot;
19
+ }, ctx: JobContext): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import { normalizeError } from "../../../../errors/normalize";
2
+ import { assignPrizeDrawWinner } from "./prizeDrawAssignWinner";
3
+ const PRODUCT_COLLECTION = "products";
4
+ /**
5
+ * Instant-mode prize-draw winner assignment. Fires when an order's
6
+ * paymentStatus transitions into "paid" — not at order creation, since a
7
+ * prize-draw order isn't guaranteed to be paid yet at that point (manual
8
+ * bank/UPI payments are confirmed later by the seller/admin; Razorpay
9
+ * confirms near-instantly but still goes through this same status flip).
10
+ */
11
+ export async function handlePrizeDrawPaymentConfirmed(input, ctx) {
12
+ const { orderId, order } = input;
13
+ if (!order.prizeDrawProductId)
14
+ return;
15
+ if (!order.items?.some((it) => it.listingType === "prize-draw"))
16
+ return;
17
+ let product;
18
+ try {
19
+ const snap = await ctx.db.collection(PRODUCT_COLLECTION).doc(order.prizeDrawProductId).get();
20
+ product = snap.exists ? snap.data() : undefined;
21
+ }
22
+ catch (err) {
23
+ void normalizeError(err);
24
+ ctx.logger.error("Failed to load prize-draw product for instant reveal", err, { orderId });
25
+ return;
26
+ }
27
+ if (!product || product.prizeRevealMode !== "instant")
28
+ return;
29
+ await assignPrizeDrawWinner(ctx, { orderId, productId: order.prizeDrawProductId });
30
+ }
@@ -5,8 +5,8 @@ import type { JobContext } from "../runtime/types";
5
5
  * its `paymentDeadline`. Runs every 5 minutes (`EVERY_5_MIN`), so an order
6
6
  * can overshoot its 15-minute deadline by up to ~5 minutes before this
7
7
  * catches it — acceptable, matches the existing tight-interval precedent
8
- * (`prizeRevealOpen`/`prizeRevealClose`) rather than inventing a 1-minute
9
- * cron that would multiply invocation cost for marginal UX gain.
8
+ * (`prizeDrawExpiryReveal`) rather than inventing a 1-minute cron that would
9
+ * multiply invocation cost for marginal UX gain.
10
10
  *
11
11
  * Orders that already have a `paymentProofUrl` are excluded by the
12
12
  * repository query — those are the 2-hour auto-approve sweep's domain
@@ -11,8 +11,8 @@ import { ORDER_MESSAGES } from "../handlers/messages";
11
11
  * its `paymentDeadline`. Runs every 5 minutes (`EVERY_5_MIN`), so an order
12
12
  * can overshoot its 15-minute deadline by up to ~5 minutes before this
13
13
  * catches it — acceptable, matches the existing tight-interval precedent
14
- * (`prizeRevealOpen`/`prizeRevealClose`) rather than inventing a 1-minute
15
- * cron that would multiply invocation cost for marginal UX gain.
14
+ * (`prizeDrawExpiryReveal`) rather than inventing a 1-minute cron that would
15
+ * multiply invocation cost for marginal UX gain.
16
16
  *
17
17
  * Orders that already have a `paymentProofUrl` are excluded by the
18
18
  * repository query — those are the 2-hour auto-approve sweep's domain
@@ -0,0 +1,20 @@
1
+ import type { JobContext } from "../runtime/types";
2
+ import type { OrderDocument } from "../../../../features/orders/schemas/firestore";
3
+ export interface AssignPrizeDrawWinnerResult {
4
+ kind: "revealed" | "already_revealed" | "pool_exhausted" | "not_eligible";
5
+ prizeWon?: OrderDocument["prizeWon"];
6
+ }
7
+ /**
8
+ * The single place a prize-draw winner is ever picked. Called by every
9
+ * trigger point (payment-confirmed for "instant" mode, sellout-detection,
10
+ * and the expiry sweep for "scheduled" mode) — never call the underlying
11
+ * transaction logic anywhere else.
12
+ *
13
+ * Preserves the exact `crypto.randomInt` rejection-sampled fairness RNG that
14
+ * buyers are shown a public GitHub source link for (product.prizeGithubFileUrl)
15
+ * — do not change the selection algorithm.
16
+ */
17
+ export declare function assignPrizeDrawWinner(ctx: JobContext, args: {
18
+ orderId: string;
19
+ productId: string;
20
+ }): Promise<AssignPrizeDrawWinnerResult>;
@@ -0,0 +1,113 @@
1
+ import crypto from "node:crypto";
2
+ import { normalizeError } from "../../../../errors/normalize";
3
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
4
+ const PRODUCT_COLLECTION = "products";
5
+ const ORDER_COLLECTION = "orders";
6
+ /**
7
+ * The single place a prize-draw winner is ever picked. Called by every
8
+ * trigger point (payment-confirmed for "instant" mode, sellout-detection,
9
+ * and the expiry sweep for "scheduled" mode) — never call the underlying
10
+ * transaction logic anywhere else.
11
+ *
12
+ * Preserves the exact `crypto.randomInt` rejection-sampled fairness RNG that
13
+ * buyers are shown a public GitHub source link for (product.prizeGithubFileUrl)
14
+ * — do not change the selection algorithm.
15
+ */
16
+ export async function assignPrizeDrawWinner(ctx, args) {
17
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
18
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping winner assignment");
19
+ return { kind: "not_eligible" };
20
+ }
21
+ const { orderId, productId } = args;
22
+ const orderRef = ctx.db.collection(ORDER_COLLECTION).doc(orderId);
23
+ const productRef = ctx.db.collection(PRODUCT_COLLECTION).doc(productId);
24
+ const result = await ctx.db.runTransaction(async (tx) => {
25
+ const [productSnap, orderSnap] = await Promise.all([
26
+ tx.get(productRef),
27
+ tx.get(orderRef),
28
+ ]);
29
+ if (!productSnap.exists || !orderSnap.exists) {
30
+ return { kind: "not_eligible" };
31
+ }
32
+ const product = productSnap.data();
33
+ const order = orderSnap.data();
34
+ // Idempotent — a winner was already assigned to this order.
35
+ if (order.prizeWon) {
36
+ return { kind: "already_revealed", prizeWon: order.prizeWon };
37
+ }
38
+ if (order.paymentStatus !== "paid" ||
39
+ order.status === "cancelled" ||
40
+ order.status === "refunded") {
41
+ return { kind: "not_eligible" };
42
+ }
43
+ const items = Array.isArray(product.prizeDrawItems)
44
+ ? [...product.prizeDrawItems]
45
+ : [];
46
+ const unwonIndices = items
47
+ .map((it, idx) => (it.isWon ? -1 : idx))
48
+ .filter((idx) => idx >= 0);
49
+ if (unwonIndices.length === 0) {
50
+ // Defensive only — preflightChecks already blocks overselling past
51
+ // prizeMaxEntries, so this should be unreachable outside a genuine
52
+ // race between two concurrent checkouts. No auto-refund here (that
53
+ // was the old buyer-claim-deadline behavior, now retired) — just
54
+ // report it so the caller can log/notify.
55
+ return { kind: "pool_exhausted" };
56
+ }
57
+ // crypto.randomInt is rejection-sampled and uniform — safe for fairness.
58
+ const pickPosition = crypto.randomInt(0, unwonIndices.length);
59
+ const pickIndex = unwonIndices[pickPosition];
60
+ const winningItem = items[pickIndex];
61
+ items[pickIndex] = { ...winningItem, isWon: true, wonByOrderId: orderId };
62
+ tx.update(productRef, {
63
+ prizeDrawItems: items,
64
+ updatedAt: ctx.now,
65
+ });
66
+ const prizeWon = {
67
+ itemNumber: winningItem.itemNumber,
68
+ title: winningItem.title,
69
+ images: winningItem.images,
70
+ wonAt: ctx.now,
71
+ };
72
+ const orderItems = Array.isArray(order.items) ? [...order.items] : [];
73
+ const revealedItems = orderItems.map((it) => it.listingType === "prize-draw" ? { ...it, prizeRevealStatus: "revealed" } : it);
74
+ tx.update(orderRef, {
75
+ prizeWon,
76
+ ...(revealedItems.length > 0 ? { items: revealedItems } : {}),
77
+ updatedAt: ctx.now,
78
+ });
79
+ return { kind: "revealed", prizeWon, userId: order.userId, productTitle: product.title };
80
+ });
81
+ if (result.kind === "revealed") {
82
+ ctx.logger.info("Prize-draw winner assigned", {
83
+ orderId,
84
+ productId,
85
+ itemNumber: result.prizeWon.itemNumber,
86
+ });
87
+ if (result.userId) {
88
+ try {
89
+ await sendNotification({
90
+ userId: result.userId,
91
+ type: "prize_won",
92
+ priority: "high",
93
+ title: "Your prize is ready!",
94
+ message: `You won "${result.prizeWon.title}" in "${result.productTitle ?? "the draw"}" — check your order for details.`,
95
+ relatedId: orderId,
96
+ relatedType: "order",
97
+ actionUrl: `/user/orders/view/${orderId}`,
98
+ });
99
+ }
100
+ catch (err) {
101
+ void normalizeError(err);
102
+ ctx.logger.warn("Prize-won notification failed", {
103
+ err: err instanceof Error ? err.message : String(err),
104
+ });
105
+ }
106
+ }
107
+ return { kind: "revealed", prizeWon: result.prizeWon };
108
+ }
109
+ if (result.kind === "pool_exhausted") {
110
+ ctx.logger.warn("Prize-draw pool exhausted at assignment time", { orderId, productId });
111
+ }
112
+ return result;
113
+ }
@@ -0,0 +1,14 @@
1
+ import type { JobContext } from "../runtime/types";
2
+ /**
3
+ * "Scheduled" reveal mode, expiry half. Fires once a draw's
4
+ * prizeRevealWindowEnd passes: assigns a winner to every paid, unrevealed
5
+ * order for that draw, then closes it. Any prize items with no corresponding
6
+ * order simply stay unrevealed / with the seller — that's expected on a
7
+ * partial sellout, not an error.
8
+ *
9
+ * The sellout half of "scheduled" mode lives in prizeDrawSoldOutReveal.ts —
10
+ * whichever fires first (expiry here, or a full sellout there) closes the
11
+ * draw; this sweep naturally no-ops afterward since the product is already
12
+ * "closed".
13
+ */
14
+ export declare function runPrizeDrawExpiryReveal(ctx: JobContext): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import { normalizeError } from "../../../../errors/normalize";
2
+ import { PRODUCT_FIELDS, ORDER_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
3
+ import { assignPrizeDrawWinner } from "./prizeDrawAssignWinner";
4
+ const PRODUCT_COLLECTION = "products";
5
+ const ORDER_COLLECTION = "orders";
6
+ const PRIZE_DRAW_LISTING_TYPE = "prize-draw";
7
+ /**
8
+ * "Scheduled" reveal mode, expiry half. Fires once a draw's
9
+ * prizeRevealWindowEnd passes: assigns a winner to every paid, unrevealed
10
+ * order for that draw, then closes it. Any prize items with no corresponding
11
+ * order simply stay unrevealed / with the seller — that's expected on a
12
+ * partial sellout, not an error.
13
+ *
14
+ * The sellout half of "scheduled" mode lives in prizeDrawSoldOutReveal.ts —
15
+ * whichever fires first (expiry here, or a full sellout there) closes the
16
+ * draw; this sweep naturally no-ops afterward since the product is already
17
+ * "closed".
18
+ */
19
+ export async function runPrizeDrawExpiryReveal(ctx) {
20
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
21
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize-draw expiry reveal");
22
+ return;
23
+ }
24
+ ctx.logger.info("Prize-draw expiry reveal sweep starting");
25
+ const snap = await ctx.db
26
+ .collection(PRODUCT_COLLECTION)
27
+ .where(PRODUCT_FIELDS.LISTING_TYPE, "==", PRIZE_DRAW_LISTING_TYPE)
28
+ .where(PRODUCT_FIELDS.PRIZE_REVEAL_STATUS, "==", PRODUCT_FIELDS.PRIZE_REVEAL_STATUS_VALUES.OPEN)
29
+ .where(PRODUCT_FIELDS.PRIZE_REVEAL_WINDOW_END, "<=", ctx.now)
30
+ .limit(100)
31
+ .get();
32
+ if (snap.empty) {
33
+ ctx.logger.info("No prize draws past expiry");
34
+ return;
35
+ }
36
+ for (const doc of snap.docs) {
37
+ const product = doc.data();
38
+ if (product.prizeDrawMode === "lottery")
39
+ continue; // out of scope — separate mechanic
40
+ const orders = await ctx.db
41
+ .collection(ORDER_COLLECTION)
42
+ .where(ORDER_FIELDS.PRIZE_DRAW_PRODUCT_ID, "==", doc.id)
43
+ .where(ORDER_FIELDS.PAYMENT_STATUS, "==", ORDER_FIELDS.PAYMENT_STATUS_VALUES.PAID)
44
+ .where(ORDER_FIELDS.STATUS, "in", [
45
+ ORDER_FIELDS.STATUS_VALUES.PENDING,
46
+ ORDER_FIELDS.STATUS_VALUES.CONFIRMED,
47
+ ORDER_FIELDS.STATUS_VALUES.PROCESSING,
48
+ ])
49
+ .get();
50
+ let revealed = 0;
51
+ for (const orderDoc of orders.docs) {
52
+ const order = orderDoc.data();
53
+ if (order.prizeWon)
54
+ continue;
55
+ try {
56
+ const result = await assignPrizeDrawWinner(ctx, {
57
+ orderId: orderDoc.id,
58
+ productId: doc.id,
59
+ });
60
+ if (result.kind === "revealed")
61
+ revealed++;
62
+ }
63
+ catch (err) {
64
+ void normalizeError(err);
65
+ ctx.logger.error("Expiry reveal failed for order", err, { orderId: orderDoc.id, productId: doc.id });
66
+ }
67
+ }
68
+ await doc.ref.update({
69
+ [PRODUCT_FIELDS.PRIZE_REVEAL_STATUS]: PRODUCT_FIELDS.PRIZE_REVEAL_STATUS_VALUES.CLOSED,
70
+ [COMMON_FIELDS.UPDATED_AT]: ctx.now,
71
+ });
72
+ ctx.logger.info("Closed prize draw at expiry", { productId: doc.id, revealed });
73
+ }
74
+ }
@@ -0,0 +1,22 @@
1
+ import type { JobContext } from "../runtime/types";
2
+ export interface PrizeDrawStockSnapshot {
3
+ listingType?: string;
4
+ prizeDrawMode?: string;
5
+ prizeRevealMode?: string;
6
+ prizeRevealStatus?: string;
7
+ prizeMaxEntries?: number;
8
+ prizeCurrentEntries?: number;
9
+ }
10
+ export interface HandlePrizeDrawSoldOutInput {
11
+ productId: string;
12
+ before: PrizeDrawStockSnapshot | null;
13
+ after: PrizeDrawStockSnapshot | null;
14
+ }
15
+ /**
16
+ * "Scheduled" reveal mode, sellout half — the other trigger condition besides
17
+ * expiry (prizeDrawExpiryReveal.ts). Fires the instant a scheduled-mode draw's
18
+ * last entry sells, so buyers don't have to wait for the expiry sweep once
19
+ * every ticket is gone. Assigns a winner to every paid, unrevealed order for
20
+ * the draw, then closes it.
21
+ */
22
+ export declare function handlePrizeDrawSoldOut(input: HandlePrizeDrawSoldOutInput, ctx: JobContext): Promise<void>;
@@ -0,0 +1,68 @@
1
+ import { normalizeError } from "../../../../errors/normalize";
2
+ import { PRODUCT_FIELDS, ORDER_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
3
+ import { assignPrizeDrawWinner } from "./prizeDrawAssignWinner";
4
+ const ORDER_COLLECTION = "orders";
5
+ function isSoldOut(doc) {
6
+ if (!doc)
7
+ return false;
8
+ const max = doc.prizeMaxEntries ?? 0;
9
+ if (max <= 0)
10
+ return false;
11
+ return (doc.prizeCurrentEntries ?? 0) >= max;
12
+ }
13
+ /**
14
+ * "Scheduled" reveal mode, sellout half — the other trigger condition besides
15
+ * expiry (prizeDrawExpiryReveal.ts). Fires the instant a scheduled-mode draw's
16
+ * last entry sells, so buyers don't have to wait for the expiry sweep once
17
+ * every ticket is gone. Assigns a winner to every paid, unrevealed order for
18
+ * the draw, then closes it.
19
+ */
20
+ export async function handlePrizeDrawSoldOut(input, ctx) {
21
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true")
22
+ return;
23
+ const { productId, before, after } = input;
24
+ if (!after)
25
+ return;
26
+ if (after.listingType !== "prize-draw")
27
+ return;
28
+ if (after.prizeDrawMode === "lottery")
29
+ return; // out of scope — separate mechanic
30
+ if (after.prizeRevealMode !== "scheduled")
31
+ return;
32
+ if (after.prizeRevealStatus !== PRODUCT_FIELDS.PRIZE_REVEAL_STATUS_VALUES.OPEN)
33
+ return;
34
+ // Only act on the crossing transition into sold-out — avoids re-firing on
35
+ // every later write to an already-closed draw.
36
+ if (isSoldOut(before) || !isSoldOut(after))
37
+ return;
38
+ const orders = await ctx.db
39
+ .collection(ORDER_COLLECTION)
40
+ .where(ORDER_FIELDS.PRIZE_DRAW_PRODUCT_ID, "==", productId)
41
+ .where(ORDER_FIELDS.PAYMENT_STATUS, "==", ORDER_FIELDS.PAYMENT_STATUS_VALUES.PAID)
42
+ .where(ORDER_FIELDS.STATUS, "in", [
43
+ ORDER_FIELDS.STATUS_VALUES.PENDING,
44
+ ORDER_FIELDS.STATUS_VALUES.CONFIRMED,
45
+ ORDER_FIELDS.STATUS_VALUES.PROCESSING,
46
+ ])
47
+ .get();
48
+ let revealed = 0;
49
+ for (const orderDoc of orders.docs) {
50
+ const order = orderDoc.data();
51
+ if (order.prizeWon)
52
+ continue;
53
+ try {
54
+ const result = await assignPrizeDrawWinner(ctx, { orderId: orderDoc.id, productId });
55
+ if (result.kind === "revealed")
56
+ revealed++;
57
+ }
58
+ catch (err) {
59
+ void normalizeError(err);
60
+ ctx.logger.error("Sellout reveal failed for order", err, { orderId: orderDoc.id, productId });
61
+ }
62
+ }
63
+ await ctx.db.collection("products").doc(productId).update({
64
+ [PRODUCT_FIELDS.PRIZE_REVEAL_STATUS]: PRODUCT_FIELDS.PRIZE_REVEAL_STATUS_VALUES.CLOSED,
65
+ [COMMON_FIELDS.UPDATED_AT]: ctx.now,
66
+ });
67
+ ctx.logger.info("Closed prize draw on sellout", { productId, revealed });
68
+ }
@@ -38,11 +38,10 @@ export { onStoreWriteHandler } from "./onStoreWrite";
38
38
  export { adminAnalyticsHandler, type AdminAnalyticsResult } from "./adminAnalytics";
39
39
  export { storeAnalyticsHandler, type StoreAnalyticsInput, type StoreAnalyticsResult, } from "./storeAnalytics";
40
40
  export { listingProcessorHandler, supportedListingCollections, type ListingRequestBody, type ListingResponseBody, } from "./listingProcessor";
41
- export { prizeRevealOpenHandler } from "./prizeRevealOpen";
42
- export { prizeRevealCloseHandler } from "./prizeRevealClose";
43
- export { prizeRevealExpiryHandler } from "./prizeRevealExpiry";
44
- export { prizeRevealReminderHandler } from "./prizeRevealReminder";
45
41
  export { bundleStockSyncHandler } from "./bundleStockSync";
42
+ export { onPrizeDrawPaymentConfirmedHandler } from "./onPrizeDrawPaymentConfirmed";
43
+ export { prizeDrawSoldOutRevealHandler } from "./prizeDrawSoldOutReveal";
44
+ export { prizeDrawExpiryRevealHandler } from "./prizeDrawExpiryReveal";
46
45
  export { onProductStockChangeHandler } from "./onProductStockChange";
47
46
  export { triggerEventRaffleHandler } from "./triggerEventRaffle";
48
47
  export { assignSpinPrizeHandler } from "./assignSpinPrize";