@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.
- package/dist/_internal/server/features/auth/actions.d.ts +0 -1
- package/dist/_internal/server/features/auth/actions.js +0 -1
- package/dist/_internal/server/features/bundles/data.d.ts +22 -4
- package/dist/_internal/server/features/bundles/data.js +41 -4
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.d.ts +15 -0
- package/dist/_internal/server/features/checkout/actions.js +249 -257
- package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
- package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
- package/dist/_internal/server/features/orders/adapters.js +1 -14
- package/dist/_internal/server/features/products/actions.js +8 -3
- package/dist/_internal/server/features/products/service.d.ts +15 -0
- package/dist/_internal/server/features/products/service.js +41 -0
- package/dist/_internal/server/functions/firestore.d.ts +3 -1
- package/dist/_internal/server/functions/firestore.js +17 -1
- package/dist/_internal/server/functions/scheduled.d.ts +3 -5
- package/dist/_internal/server/functions/scheduled.js +14 -30
- package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
- package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
- package/dist/_internal/server/jobs/core/index.d.ts +4 -4
- package/dist/_internal/server/jobs/core/index.js +4 -4
- package/dist/_internal/server/jobs/core/jobRunners.js +10 -0
- package/dist/_internal/server/jobs/core/newsletterExport.d.ts +15 -0
- package/dist/_internal/server/jobs/core/newsletterExport.js +43 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +3 -2
- package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
- package/dist/_internal/server/jobs/core/revenueRollup.d.ts +13 -0
- package/dist/_internal/server/jobs/core/revenueRollup.js +22 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +82 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +4 -4
- package/dist/_internal/server/jobs/handlers/index.js +9 -6
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
- package/dist/_internal/server/jobs/handlers/revenueRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/revenueRollup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +40 -10
- package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
- package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
- package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
- package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
- package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
- package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
- package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +7 -0
- package/dist/_internal/shared/features/checkout/config.js +7 -0
- package/dist/_internal/shared/fees/calculator.d.ts +26 -0
- package/dist/_internal/shared/fees/calculator.js +25 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +5 -5
- package/dist/constants/field-names.d.ts +17 -1
- package/dist/constants/field-names.js +17 -1
- package/dist/features/about/components/FeesView.js +18 -0
- package/dist/features/account/hooks/useNotifications.js +5 -0
- package/dist/features/admin/actions/admin-actions.js +6 -0
- package/dist/features/admin/actions/notification-actions.d.ts +4 -1
- package/dist/features/admin/actions/notification-actions.js +57 -12
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
- package/dist/features/admin/components/AdminNewsletterView.js +42 -11
- package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +115 -11
- package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
- package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
- package/dist/features/admin/hooks/useAdminListing.js +4 -2
- package/dist/features/admin/repository/analytics-rollup.repository.d.ts +25 -0
- package/dist/features/admin/repository/analytics-rollup.repository.js +31 -0
- package/dist/features/admin/repository/site-settings.repository.js +8 -1
- package/dist/features/admin/schemas/firestore.d.ts +54 -5
- package/dist/features/admin/schemas/firestore.js +63 -26
- package/dist/features/auctions/actions/bid-actions.js +3 -1
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
- package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
- package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
- package/dist/features/auctions/components/CollapsibleBidHistory.js +21 -3
- package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
- package/dist/features/auctions/components/LiveBidPrice.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
- package/dist/features/auctions/hooks/useBids.d.ts +33 -0
- package/dist/features/auctions/hooks/useBids.js +32 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
- package/dist/features/auth/checkout-value-otp.d.ts +7 -10
- package/dist/features/auth/checkout-value-otp.js +14 -10
- package/dist/features/auth/server.d.ts +0 -1
- package/dist/features/auth/server.js +0 -1
- package/dist/features/blog/api/[slug]/route.d.ts +5 -0
- package/dist/features/blog/api/[slug]/route.js +12 -3
- package/dist/features/blog/components/BlogPostView.d.ts +2 -0
- package/dist/features/blog/components/BlogPostView.js +5 -4
- package/dist/features/blog/hooks/useBlog.d.ts +2 -0
- package/dist/features/blog/hooks/useBlog.js +2 -0
- package/dist/features/blog/repository/blog.repository.d.ts +7 -0
- package/dist/features/blog/repository/blog.repository.js +44 -0
- package/dist/features/blog/schemas/firestore.d.ts +3 -0
- package/dist/features/blog/schemas/firestore.js +1 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/index.d.ts +0 -2
- package/dist/features/cart/components/index.js +0 -1
- package/dist/features/cart/hooks/useCartCount.js +22 -5
- package/dist/features/cart/hooks/useGuestCart.js +11 -1
- package/dist/features/cart/utils/guest-cart.d.ts +4 -0
- package/dist/features/cart/utils/guest-cart.js +10 -0
- package/dist/features/cart/utils/pending-ops.d.ts +17 -0
- package/dist/features/cart/utils/pending-ops.js +46 -5
- package/dist/features/categories/components/BrandDetailPageView.js +6 -2
- package/dist/features/categories/components/BundleDetailView.js +4 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
- package/dist/features/categories/schemas/firestore.d.ts +7 -0
- package/dist/features/checkout/actions/index.d.ts +0 -1
- package/dist/features/checkout/actions/index.js +0 -1
- package/dist/features/checkout/schemas/firestore.d.ts +2 -2
- package/dist/features/events/actions/event-actions.d.ts +2 -0
- package/dist/features/events/actions/event-actions.js +5 -0
- package/dist/features/events/components/EventCard.js +1 -2
- package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
- package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
- package/dist/features/events/components/index.d.ts +2 -0
- package/dist/features/events/components/index.js +1 -0
- package/dist/features/events/repository/events.repository.d.ts +6 -0
- package/dist/features/events/repository/events.repository.js +21 -0
- package/dist/features/history/repository/user-history.repository.d.ts +1 -1
- package/dist/features/history/utils/guest-history.d.ts +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
- package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/layout/BackToTop.d.ts +9 -5
- package/dist/features/layout/BackToTop.js +17 -21
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.d.ts +7 -0
- package/dist/features/media/MediaVideo.js +36 -5
- package/dist/features/orders/actions/refund-actions.js +1 -0
- package/dist/features/orders/components/OrdersList.js +1 -5
- package/dist/features/orders/schemas/firestore.d.ts +16 -8
- package/dist/features/orders/schemas/index.d.ts +3 -32
- package/dist/features/orders/schemas/index.js +1 -4
- package/dist/features/orders/types/index.d.ts +6 -9
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
- package/dist/features/products/actions/product-actions.js +13 -10
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
- package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
- package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
- package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
- package/dist/features/products/components/PrizeRevealModal.js +18 -147
- package/dist/features/products/components/ProductDetailPageView.js +72 -30
- package/dist/features/products/components/ProductForm.js +7 -14
- package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
- package/dist/features/products/components/ProductGalleryClient.js +29 -10
- package/dist/features/products/components/ProductsIndexListing.js +2 -1
- package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +1 -1
- package/dist/features/products/components/index.d.ts +4 -2
- package/dist/features/products/components/index.js +1 -0
- package/dist/features/products/constants/action-defs.d.ts +28 -4
- package/dist/features/products/constants/action-defs.js +76 -3
- package/dist/features/products/repository/products.repository.d.ts +6 -0
- package/dist/features/products/repository/products.repository.js +20 -1
- package/dist/features/products/schemas/firestore.d.ts +26 -2
- package/dist/features/products/schemas/index.d.ts +11 -3
- package/dist/features/products/schemas/index.js +10 -2
- package/dist/features/products/types/index.d.ts +2 -1
- package/dist/features/reviews/actions/review-actions.js +1 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
- package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
- package/dist/features/reviews/components/ReviewsList.js +2 -1
- package/dist/features/reviews/schemas/firestore.d.ts +3 -0
- package/dist/features/reviews/schemas/index.d.ts +6 -0
- package/dist/features/reviews/schemas/index.js +2 -0
- package/dist/features/reviews/types/index.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.js +3 -2
- package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
- package/dist/features/scams/repository/scammer.repository.js +24 -0
- package/dist/features/seller/actions/seller-actions.js +8 -0
- package/dist/features/seller/components/SellerOrdersView.js +1 -1
- package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
- package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
- package/dist/features/stores/components/StoreNavTabs.js +21 -9
- package/dist/features/tester/actions/index.d.ts +1 -0
- package/dist/features/tester/actions/index.js +1 -0
- package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
- package/dist/features/tester/actions/leaderboard-actions.js +5 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
- package/dist/features/tester/components/index.d.ts +2 -0
- package/dist/features/tester/components/index.js +1 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
- package/dist/features/tester/schemas/firestore.d.ts +22 -1
- package/dist/features/tester/schemas/firestore.js +8 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
- package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
- package/dist/features/tester/seed-data/products-tester-seed-data.js +152 -8
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +249 -14
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +10 -1
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +47 -0
- package/dist/features/whatsapp-bot/types/index.d.ts +19 -0
- package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
- package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
- package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
- package/dist/index.d.ts +23 -24
- package/dist/index.js +29 -60
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +2 -2
- package/dist/next/routing/route-map.d.ts +4 -0
- package/dist/next/routing/route-map.js +2 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +6 -26
- package/dist/seed/bids-seed-data.js +14 -2
- package/dist/seed/blog-posts-seed-data.js +2 -1
- package/dist/seed/categories-seed-data.js +11 -2
- package/dist/seed/events-seed-data.js +11 -11
- package/dist/seed/faq-seed-data.js +5 -1
- package/dist/seed/products-auctions-seed-data.js +2 -2
- package/dist/seed/products-preorders-seed-data.d.ts +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +56 -10
- package/dist/seed/products-standard-seed-data.js +82 -3
- package/dist/seed/scammers-seed-data.js +75 -2
- package/dist/seed/site-settings-seed-data.js +12 -2
- package/dist/server-entry.d.ts +0 -1
- package/dist/server-entry.js +0 -1
- package/dist/server.d.ts +6 -22
- package/dist/server.js +12 -59
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.js +1 -1
- package/dist/ui/components/ImageLightbox.d.ts +9 -0
- package/dist/ui/components/ImageLightbox.js +6 -3
- package/dist/ui/components/ListingToolbar.d.ts +18 -1
- package/dist/ui/components/ListingToolbar.js +5 -2
- package/dist/ui/components/StickyToolbar.js +13 -8
- package/package.json +1 -1
|
@@ -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
|
-
* (`
|
|
9
|
-
*
|
|
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
|
-
* (`
|
|
15
|
-
*
|
|
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
|
|
@@ -51,6 +51,7 @@ export async function runPaymentWindowTimeout(ctx) {
|
|
|
51
51
|
message: ORDER_MESSAGES.CANCELLED_PAYMENT_WINDOW_MESSAGE(entry.data.productTitle),
|
|
52
52
|
relatedId: entry.id,
|
|
53
53
|
relatedType: "order",
|
|
54
|
+
orderWhatsappAddonPaid: entry.data.whatsappNotifyAddon === true,
|
|
54
55
|
})));
|
|
55
56
|
ctx.logger.info("Payment window timeout sweep complete", {
|
|
56
57
|
scanned: expired.length,
|
|
@@ -49,6 +49,7 @@ export async function runPendingOrderTimeout(ctx) {
|
|
|
49
49
|
message: ORDER_MESSAGES.CANCELLED_TIMEOUT_MESSAGE(entry.data.productTitle, timeoutHours),
|
|
50
50
|
relatedId: entry.id,
|
|
51
51
|
relatedType: "order",
|
|
52
|
+
orderWhatsappAddonPaid: entry.data.whatsappNotifyAddon === true,
|
|
52
53
|
})));
|
|
53
54
|
ctx.logger.info("Pending order timeout complete", { cancelled: timedOut.length, restored });
|
|
54
55
|
}
|
|
@@ -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("prizeDrawProductId", "==", 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("prizeDrawProductId", "==", 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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: daily revenue rollup.
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/admin/dashboard` used to sum `totalPrice` over the FULL
|
|
5
|
+
* `delivered`-status orders list on every request (`orderRepository.findByStatus`
|
|
6
|
+
* has no bound) — a real, growing Firestore-read cost and a 10s-timeout risk
|
|
7
|
+
* on Vercel Hobby (CLAUDE.md Rule #6) as order volume increases. This job
|
|
8
|
+
* pre-computes the same aggregate once a day and writes it to the
|
|
9
|
+
* `analytics/dashboardRollup` singleton doc; the API route now reads that
|
|
10
|
+
* single doc instead of scanning the collection.
|
|
11
|
+
*/
|
|
12
|
+
import type { JobContext } from "../runtime/types";
|
|
13
|
+
export declare function runRevenueRollup(ctx: JobContext): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: daily revenue rollup.
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/admin/dashboard` used to sum `totalPrice` over the FULL
|
|
5
|
+
* `delivered`-status orders list on every request (`orderRepository.findByStatus`
|
|
6
|
+
* has no bound) — a real, growing Firestore-read cost and a 10s-timeout risk
|
|
7
|
+
* on Vercel Hobby (CLAUDE.md Rule #6) as order volume increases. This job
|
|
8
|
+
* pre-computes the same aggregate once a day and writes it to the
|
|
9
|
+
* `analytics/dashboardRollup` singleton doc; the API route now reads that
|
|
10
|
+
* single doc instead of scanning the collection.
|
|
11
|
+
*/
|
|
12
|
+
import { orderRepository } from "../../../../repositories";
|
|
13
|
+
import { analyticsRollupRepository } from "../../../../repositories";
|
|
14
|
+
export async function runRevenueRollup(ctx) {
|
|
15
|
+
const deliveredOrders = await orderRepository.findByStatus("delivered").catch(() => []);
|
|
16
|
+
const totalRevenue = deliveredOrders.reduce((sum, order) => sum + (Number(order.totalPrice ?? 0) || 0), 0);
|
|
17
|
+
await analyticsRollupRepository.setDashboardRollup({
|
|
18
|
+
totalRevenue,
|
|
19
|
+
deliveredOrderCount: deliveredOrders.length,
|
|
20
|
+
});
|
|
21
|
+
ctx.logger.info("revenueRollup: complete", { totalRevenue, deliveredOrderCount: deliveredOrders.length });
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: async WhatsApp notification dispatch (`whatsappNotify` job).
|
|
3
|
+
*
|
|
4
|
+
* `sendNotification()` (appkit/src/features/admin/actions/notification-actions.ts)
|
|
5
|
+
* no longer calls the Meta Cloud API synchronously in-request — it enqueues
|
|
6
|
+
* this job instead (CLAUDE.md Rule #6 async job primitive) and returns
|
|
7
|
+
* immediately. This runner does the actual send, with a bounded retry, and
|
|
8
|
+
* writes the outcome back onto `notifications/{notificationId}` so admins
|
|
9
|
+
* have visibility beyond the `jobs/{jobId}` doc itself.
|
|
10
|
+
*
|
|
11
|
+
* Retry note: `sendWhatsAppTemplateMessage`/`sendWhatsAppBusinessMessage`
|
|
12
|
+
* return a plain boolean (not a status code), so this runner cannot cleanly
|
|
13
|
+
* distinguish a transient 5xx from a terminal 4xx (bad token, unknown
|
|
14
|
+
* template) the way a status-code-aware client could. It retries any
|
|
15
|
+
* failure up to MAX_ATTEMPTS with a short backoff — an acceptable
|
|
16
|
+
* simplification since this runs off the critical path, well inside the
|
|
17
|
+
* Firebase Function's 300s budget, and a terminal error just burns two
|
|
18
|
+
* extra fast failures before giving up.
|
|
19
|
+
*/
|
|
20
|
+
import type { JobContext } from "../runtime/types";
|
|
21
|
+
import type { JobRunResult } from "./jobRunners";
|
|
22
|
+
export interface WhatsAppNotifyPayload {
|
|
23
|
+
toPhone: string;
|
|
24
|
+
title: string;
|
|
25
|
+
message: string;
|
|
26
|
+
type: string;
|
|
27
|
+
/** Approved Meta template name for this notification type, if configured. */
|
|
28
|
+
templateName?: string;
|
|
29
|
+
/** BCP-47 language code the template was approved in. Defaults to "en". */
|
|
30
|
+
templateLanguage?: string;
|
|
31
|
+
notificationId: string;
|
|
32
|
+
phoneNumberId: string;
|
|
33
|
+
accessToken: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function runWhatsAppNotify(payload: WhatsAppNotifyPayload, ctx: JobContext): Promise<JobRunResult>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: async WhatsApp notification dispatch (`whatsappNotify` job).
|
|
3
|
+
*
|
|
4
|
+
* `sendNotification()` (appkit/src/features/admin/actions/notification-actions.ts)
|
|
5
|
+
* no longer calls the Meta Cloud API synchronously in-request — it enqueues
|
|
6
|
+
* this job instead (CLAUDE.md Rule #6 async job primitive) and returns
|
|
7
|
+
* immediately. This runner does the actual send, with a bounded retry, and
|
|
8
|
+
* writes the outcome back onto `notifications/{notificationId}` so admins
|
|
9
|
+
* have visibility beyond the `jobs/{jobId}` doc itself.
|
|
10
|
+
*
|
|
11
|
+
* Retry note: `sendWhatsAppTemplateMessage`/`sendWhatsAppBusinessMessage`
|
|
12
|
+
* return a plain boolean (not a status code), so this runner cannot cleanly
|
|
13
|
+
* distinguish a transient 5xx from a terminal 4xx (bad token, unknown
|
|
14
|
+
* template) the way a status-code-aware client could. It retries any
|
|
15
|
+
* failure up to MAX_ATTEMPTS with a short backoff — an acceptable
|
|
16
|
+
* simplification since this runs off the critical path, well inside the
|
|
17
|
+
* Firebase Function's 300s budget, and a terminal error just burns two
|
|
18
|
+
* extra fast failures before giving up.
|
|
19
|
+
*/
|
|
20
|
+
import { normalizeError } from "../../../../errors/normalize";
|
|
21
|
+
import { notificationRepository } from "../../../../features/admin/repository/notification.repository";
|
|
22
|
+
import { sendWhatsAppTemplateMessage, sendWhatsAppBusinessMessage, } from "../../../../features/whatsapp-bot/helpers/whatsapp";
|
|
23
|
+
const MAX_ATTEMPTS = 3;
|
|
24
|
+
const RETRY_DELAY_MS = 1500;
|
|
25
|
+
function delay(ms) {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
export async function runWhatsAppNotify(payload, ctx) {
|
|
29
|
+
const { toPhone, title, message, templateName, templateLanguage, notificationId, phoneNumberId, accessToken } = payload;
|
|
30
|
+
let sent = false;
|
|
31
|
+
let lastError;
|
|
32
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS && !sent; attempt++) {
|
|
33
|
+
try {
|
|
34
|
+
sent = templateName
|
|
35
|
+
? await sendWhatsAppTemplateMessage({
|
|
36
|
+
toPhone,
|
|
37
|
+
phoneNumberId,
|
|
38
|
+
accessToken,
|
|
39
|
+
templateName,
|
|
40
|
+
languageCode: templateLanguage ?? "en",
|
|
41
|
+
bodyParams: [title, message],
|
|
42
|
+
})
|
|
43
|
+
: await sendWhatsAppBusinessMessage({
|
|
44
|
+
toPhone,
|
|
45
|
+
phoneNumberId,
|
|
46
|
+
accessToken,
|
|
47
|
+
message: `*${title}*\n${message}`,
|
|
48
|
+
});
|
|
49
|
+
if (!sent)
|
|
50
|
+
lastError = "Meta WhatsApp API returned a non-OK response";
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
void normalizeError(err);
|
|
54
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
55
|
+
}
|
|
56
|
+
if (!sent && attempt < MAX_ATTEMPTS) {
|
|
57
|
+
ctx.logger.warn("whatsappNotify: send attempt failed, retrying", { notificationId, attempt, error: lastError ?? "" });
|
|
58
|
+
await delay(RETRY_DELAY_MS * attempt);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!templateName) {
|
|
62
|
+
ctx.logger.warn("whatsappNotify: no approved template configured for this notification type — sent as free-form text, which Meta rejects outside the 24h customer-service window", { notificationId });
|
|
63
|
+
}
|
|
64
|
+
await notificationRepository
|
|
65
|
+
.update(notificationId, { whatsappStatus: sent ? "sent" : "failed" })
|
|
66
|
+
.catch((err) => {
|
|
67
|
+
void normalizeError(err);
|
|
68
|
+
ctx.logger.warn("whatsappNotify: failed to write back whatsappStatus onto notification doc", {
|
|
69
|
+
notificationId,
|
|
70
|
+
error: err instanceof Error ? err.message : String(err),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
if (!sent) {
|
|
74
|
+
throw new Error(lastError ?? "WhatsApp send failed after retries");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
summary: { total: 1, succeeded: 1, skipped: 0, failed: 0 },
|
|
78
|
+
succeeded: [notificationId],
|
|
79
|
+
skipped: [],
|
|
80
|
+
failed: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -28,6 +28,7 @@ export { paymentWindowTimeoutHandler } from "./paymentWindowTimeout";
|
|
|
28
28
|
export { hardBanReinstatementHandler } from "./hardBanReinstatement";
|
|
29
29
|
export { paymentReviewAutoApproveHandler } from "./paymentReviewAutoApprove";
|
|
30
30
|
export { productStatsSyncHandler } from "./productStatsSync";
|
|
31
|
+
export { revenueRollupHandler } from "./revenueRollup";
|
|
31
32
|
export { positionsReconcileHandler } from "./positionsReconcile";
|
|
32
33
|
export { payoutBatchHandler } from "./payoutBatch";
|
|
33
34
|
export { weeklyPayoutEligibilityHandler } from "./weeklyPayoutEligibility";
|
|
@@ -37,11 +38,10 @@ export { onStoreWriteHandler } from "./onStoreWrite";
|
|
|
37
38
|
export { adminAnalyticsHandler, type AdminAnalyticsResult } from "./adminAnalytics";
|
|
38
39
|
export { storeAnalyticsHandler, type StoreAnalyticsInput, type StoreAnalyticsResult, } from "./storeAnalytics";
|
|
39
40
|
export { listingProcessorHandler, supportedListingCollections, type ListingRequestBody, type ListingResponseBody, } from "./listingProcessor";
|
|
40
|
-
export { prizeRevealOpenHandler } from "./prizeRevealOpen";
|
|
41
|
-
export { prizeRevealCloseHandler } from "./prizeRevealClose";
|
|
42
|
-
export { prizeRevealExpiryHandler } from "./prizeRevealExpiry";
|
|
43
|
-
export { prizeRevealReminderHandler } from "./prizeRevealReminder";
|
|
44
41
|
export { bundleStockSyncHandler } from "./bundleStockSync";
|
|
42
|
+
export { onPrizeDrawPaymentConfirmedHandler } from "./onPrizeDrawPaymentConfirmed";
|
|
43
|
+
export { prizeDrawSoldOutRevealHandler } from "./prizeDrawSoldOutReveal";
|
|
44
|
+
export { prizeDrawExpiryRevealHandler } from "./prizeDrawExpiryReveal";
|
|
45
45
|
export { onProductStockChangeHandler } from "./onProductStockChange";
|
|
46
46
|
export { triggerEventRaffleHandler } from "./triggerEventRaffle";
|
|
47
47
|
export { assignSpinPrizeHandler } from "./assignSpinPrize";
|
|
@@ -28,6 +28,7 @@ export { paymentWindowTimeoutHandler } from "./paymentWindowTimeout";
|
|
|
28
28
|
export { hardBanReinstatementHandler } from "./hardBanReinstatement";
|
|
29
29
|
export { paymentReviewAutoApproveHandler } from "./paymentReviewAutoApprove";
|
|
30
30
|
export { productStatsSyncHandler } from "./productStatsSync";
|
|
31
|
+
export { revenueRollupHandler } from "./revenueRollup";
|
|
31
32
|
export { positionsReconcileHandler } from "./positionsReconcile";
|
|
32
33
|
export { payoutBatchHandler } from "./payoutBatch";
|
|
33
34
|
export { weeklyPayoutEligibilityHandler } from "./weeklyPayoutEligibility";
|
|
@@ -37,13 +38,15 @@ export { onStoreWriteHandler } from "./onStoreWrite";
|
|
|
37
38
|
export { adminAnalyticsHandler } from "./adminAnalytics";
|
|
38
39
|
export { storeAnalyticsHandler, } from "./storeAnalytics";
|
|
39
40
|
export { listingProcessorHandler, supportedListingCollections, } from "./listingProcessor";
|
|
40
|
-
// SB1-L (S7-PrizeDraws) —
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
export { prizeRevealReminderHandler } from "./prizeRevealReminder";
|
|
41
|
+
// SB1-L (S7-PrizeDraws) — bundle sync, event raffles, and spin-the-wheel.
|
|
42
|
+
// Prize-draw reveal itself was redesigned to a fully-automatic model — see
|
|
43
|
+
// prizeDrawAssignWinner.ts (shared core), onPrizeDrawPaymentConfirmed
|
|
44
|
+
// (instant mode), prizeDrawSoldOutReveal (scheduled mode, sellout half), and
|
|
45
|
+
// prizeDrawExpiryReveal (scheduled mode, expiry half) below.
|
|
46
46
|
export { bundleStockSyncHandler } from "./bundleStockSync";
|
|
47
|
+
export { onPrizeDrawPaymentConfirmedHandler } from "./onPrizeDrawPaymentConfirmed";
|
|
48
|
+
export { prizeDrawSoldOutRevealHandler } from "./prizeDrawSoldOutReveal";
|
|
49
|
+
export { prizeDrawExpiryRevealHandler } from "./prizeDrawExpiryReveal";
|
|
47
50
|
// SB-UNI-V — Firestore onWrite trigger for product stock changes (recomputes
|
|
48
51
|
// bundleStockStatus on categoryType:"bundle" rows + activeMemberCount on
|
|
49
52
|
// groupedListings).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FirestoreTriggerHandler } from "../runtime/types";
|
|
2
|
+
import { type PrizeDrawOrderSnapshot } from "../core/onPrizeDrawPaymentConfirmed";
|
|
3
|
+
export declare const onPrizeDrawPaymentConfirmedHandler: FirestoreTriggerHandler<PrizeDrawOrderSnapshot, PrizeDrawOrderSnapshot>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { handlePrizeDrawPaymentConfirmed, } from "../core/onPrizeDrawPaymentConfirmed";
|
|
2
|
+
export const onPrizeDrawPaymentConfirmedHandler = async (event, ctx) => {
|
|
3
|
+
const before = event.before;
|
|
4
|
+
const after = event.after;
|
|
5
|
+
if (!after)
|
|
6
|
+
return;
|
|
7
|
+
// Only act on the paid transition — avoids re-firing on every other order write.
|
|
8
|
+
if (before?.paymentStatus === "paid" || after.paymentStatus !== "paid")
|
|
9
|
+
return;
|
|
10
|
+
await handlePrizeDrawPaymentConfirmed({ orderId: event.params.orderId, order: after }, ctx);
|
|
11
|
+
};
|