@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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* maxPerUser gate
|
|
3
|
-
* (SB6-C, SB8-A).
|
|
2
|
+
* maxPerUser gate for the checkout pipeline (SB6-C).
|
|
4
3
|
*
|
|
5
4
|
* `enforcePrizePoolCap` was removed in S-SBUNI-RULES (2026-05-13) — the
|
|
6
5
|
* equivalent check now lives in `prizeDrawRule.preflightChecks` in the rule
|
|
7
|
-
* registry and is invoked via `runSyncPreflight`.
|
|
6
|
+
* registry and is invoked via `runSyncPreflight`. `computePrizeRevealDeadline`
|
|
7
|
+
* (SB8-A) was removed with the buyer-claim-deadline mechanic — reveal is now
|
|
8
|
+
* fully automatic, there's nothing left for a buyer to "claim" before a
|
|
9
|
+
* deadline.
|
|
8
10
|
*/
|
|
9
11
|
import type { ProductDocument } from "../../../../features/products/schemas/firestore";
|
|
10
12
|
import type { CartItemDocument } from "../../../../features/cart/schemas/firestore";
|
|
@@ -34,16 +36,3 @@ export declare function enforceMaxPerUserForCart(args: {
|
|
|
34
36
|
product: ProductDocument;
|
|
35
37
|
}>;
|
|
36
38
|
}): Promise<void>;
|
|
37
|
-
/**
|
|
38
|
-
* SB8-A — compute the deadline by which the buyer must claim their prize
|
|
39
|
-
* via the reveal API.
|
|
40
|
-
*
|
|
41
|
-
* Rules:
|
|
42
|
-
* - If the reveal window has not yet opened:
|
|
43
|
-
* deadline = min(revealWindowStart + revealDeadlineDays, revealWindowEnd)
|
|
44
|
-
* - If the window is already open:
|
|
45
|
-
* deadline = min(now + revealDeadlineDays, revealWindowEnd)
|
|
46
|
-
*
|
|
47
|
-
* Returns `undefined` if the product lacks the prize-draw fields.
|
|
48
|
-
*/
|
|
49
|
-
export declare function computePrizeRevealDeadline(product: Pick<ProductDocument, "prizeRevealWindowStart" | "prizeRevealWindowEnd" | "prizeRevealDeadlineDays">, now?: Date): Date | undefined;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* maxPerUser gate
|
|
3
|
-
* (SB6-C, SB8-A).
|
|
2
|
+
* maxPerUser gate for the checkout pipeline (SB6-C).
|
|
4
3
|
*
|
|
5
4
|
* `enforcePrizePoolCap` was removed in S-SBUNI-RULES (2026-05-13) — the
|
|
6
5
|
* equivalent check now lives in `prizeDrawRule.preflightChecks` in the rule
|
|
7
|
-
* registry and is invoked via `runSyncPreflight`.
|
|
6
|
+
* registry and is invoked via `runSyncPreflight`. `computePrizeRevealDeadline`
|
|
7
|
+
* (SB8-A) was removed with the buyer-claim-deadline mechanic — reveal is now
|
|
8
|
+
* fully automatic, there's nothing left for a buyer to "claim" before a
|
|
9
|
+
* deadline.
|
|
8
10
|
*/
|
|
9
11
|
import { ValidationError } from "../../../../errors";
|
|
10
12
|
import { orderRepository } from "../../../../repositories";
|
|
@@ -54,30 +56,3 @@ export async function enforceMaxPerUserForCart(args) {
|
|
|
54
56
|
throw Object.assign(new ValidationError(`Purchase limit reached for one or more items. ${summary}`), { code: "MAX_PER_USER", violations });
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
|
-
/**
|
|
58
|
-
* SB8-A — compute the deadline by which the buyer must claim their prize
|
|
59
|
-
* via the reveal API.
|
|
60
|
-
*
|
|
61
|
-
* Rules:
|
|
62
|
-
* - If the reveal window has not yet opened:
|
|
63
|
-
* deadline = min(revealWindowStart + revealDeadlineDays, revealWindowEnd)
|
|
64
|
-
* - If the window is already open:
|
|
65
|
-
* deadline = min(now + revealDeadlineDays, revealWindowEnd)
|
|
66
|
-
*
|
|
67
|
-
* Returns `undefined` if the product lacks the prize-draw fields.
|
|
68
|
-
*/
|
|
69
|
-
export function computePrizeRevealDeadline(product, now = new Date()) {
|
|
70
|
-
const windowStart = product.prizeRevealWindowStart
|
|
71
|
-
? new Date(product.prizeRevealWindowStart)
|
|
72
|
-
: undefined;
|
|
73
|
-
const windowEnd = product.prizeRevealWindowEnd
|
|
74
|
-
? new Date(product.prizeRevealWindowEnd)
|
|
75
|
-
: undefined;
|
|
76
|
-
if (!windowStart || !windowEnd)
|
|
77
|
-
return undefined;
|
|
78
|
-
const days = product.prizeRevealDeadlineDays ?? 3;
|
|
79
|
-
const dayMs = 86400000;
|
|
80
|
-
const base = windowStart.getTime() > now.getTime() ? windowStart : now;
|
|
81
|
-
const candidate = new Date(base.getTime() + days * dayMs);
|
|
82
|
-
return candidate.getTime() < windowEnd.getTime() ? candidate : windowEnd;
|
|
83
|
-
}
|
|
@@ -13,12 +13,6 @@ export function orderDocumentToOrder(doc) {
|
|
|
13
13
|
...(item.prizeRevealStatus
|
|
14
14
|
? { prizeRevealStatus: item.prizeRevealStatus }
|
|
15
15
|
: {}),
|
|
16
|
-
...(item.prizeRevealDeadline
|
|
17
|
-
? { prizeRevealDeadline: item.prizeRevealDeadline }
|
|
18
|
-
: {}),
|
|
19
|
-
...(item.revealedItemNumber != null
|
|
20
|
-
? { revealedItemNumber: item.revealedItemNumber }
|
|
21
|
-
: {}),
|
|
22
16
|
...(item.cancelledQuantity != null
|
|
23
17
|
? { cancelledQuantity: item.cancelledQuantity }
|
|
24
18
|
: {}),
|
|
@@ -80,14 +74,7 @@ export function orderDocumentToOrder(doc) {
|
|
|
80
74
|
},
|
|
81
75
|
}
|
|
82
76
|
: {}),
|
|
83
|
-
...(doc.prizeRevealDeadline
|
|
84
|
-
? {
|
|
85
|
-
prizeRevealDeadline: doc.prizeRevealDeadline instanceof Date
|
|
86
|
-
? doc.prizeRevealDeadline.toISOString()
|
|
87
|
-
: String(doc.prizeRevealDeadline),
|
|
88
|
-
}
|
|
89
|
-
: {}),
|
|
90
|
-
...(doc.prizeRevealExpired !== undefined ? { prizeRevealExpired: doc.prizeRevealExpired } : {}),
|
|
91
77
|
...(doc.prizeDrawProductId ? { prizeDrawProductId: doc.prizeDrawProductId } : {}),
|
|
78
|
+
...(doc.prizeRevealMode ? { prizeRevealMode: doc.prizeRevealMode } : {}),
|
|
92
79
|
};
|
|
93
80
|
}
|
|
@@ -4,7 +4,7 @@ import { productRepository } from "../../../../repositories";
|
|
|
4
4
|
import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
|
|
5
5
|
import { isAdminUser } from "../../../../features/auth/role-predicates";
|
|
6
6
|
import { productInputSchema, productUpdateSchema, auctionInputSchema, preOrderInputSchema, setFeaturedSchema, setStatusSchema } from "../../../shared/features/products/schema";
|
|
7
|
-
import { assertProductOwnership, assertStatusTransition } from "./service";
|
|
7
|
+
import { assertProductOwnership, assertStatusTransition, assertPrizeDrawNotLocked, assertPrizeDrawWonItemsImmutable, } from "./service";
|
|
8
8
|
import { ValidationError } from "../../../shared/errors/index";
|
|
9
9
|
export async function createProductAction(input) {
|
|
10
10
|
return wrapAction(async () => {
|
|
@@ -54,17 +54,19 @@ export async function createPreOrderAction(input) {
|
|
|
54
54
|
export async function updateProductAction(productId, input) {
|
|
55
55
|
return wrapAction(async () => {
|
|
56
56
|
const user = await requireRoleUser(["seller", "admin"]);
|
|
57
|
-
await assertProductOwnership(productId, user.uid);
|
|
57
|
+
const product = await assertProductOwnership(productId, user.uid);
|
|
58
58
|
const parsed = productUpdateSchema.safeParse(input);
|
|
59
59
|
if (!parsed.success)
|
|
60
60
|
throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
|
|
61
|
+
assertPrizeDrawWonItemsImmutable(product, parsed.data.prizeDrawItems);
|
|
61
62
|
return productRepository.update(productId, parsed.data);
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
65
|
export async function deleteProductAction(productId) {
|
|
65
66
|
return wrapAction(async () => {
|
|
66
67
|
const user = await requireRoleUser(["seller", "admin"]);
|
|
67
|
-
await assertProductOwnership(productId, user.uid);
|
|
68
|
+
const product = await assertProductOwnership(productId, user.uid);
|
|
69
|
+
assertPrizeDrawNotLocked(product, "deleted");
|
|
68
70
|
return productRepository.delete(productId);
|
|
69
71
|
});
|
|
70
72
|
}
|
|
@@ -76,6 +78,9 @@ export async function setProductStatusAction(input) {
|
|
|
76
78
|
throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
|
|
77
79
|
const product = await assertProductOwnership(parsed.data.productId, user.uid);
|
|
78
80
|
assertStatusTransition(product.status, parsed.data.status);
|
|
81
|
+
if (parsed.data.status !== "published") {
|
|
82
|
+
assertPrizeDrawNotLocked(product, "unpublished or archived");
|
|
83
|
+
}
|
|
79
84
|
// SB-UNI-O — live listings must be admin-verified before a seller can publish them.
|
|
80
85
|
if (parsed.data.status === "published" &&
|
|
81
86
|
(product.listingType ?? "standard") === "live" &&
|
|
@@ -3,6 +3,21 @@ import type { ProductDocument } from "../../../shared/features/products/types";
|
|
|
3
3
|
export declare function assertProductOwnership(productId: string, sellerId: string): Promise<ProductDocument>;
|
|
4
4
|
/** Validate that a status transition is allowed. */
|
|
5
5
|
export declare function assertStatusTransition(from: string, to: string): void;
|
|
6
|
+
/**
|
|
7
|
+
* Anti-scam guard — once a prize draw has at least one paid entry, the
|
|
8
|
+
* seller can no longer take it down (unpublish/archive/delete). Prevents a
|
|
9
|
+
* seller from dodging shipment of an expensive won prize by pulling the
|
|
10
|
+
* listing after the fact.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertPrizeDrawNotLocked(product: Pick<ProductDocument, "listingType" | "prizeCurrentEntries">, action: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Anti-scam guard — once a prize item has been won, its title/images/
|
|
15
|
+
* description/condition/estimatedValue become immutable server-side (the
|
|
16
|
+
* client editor already locks this visually via PrizeDrawItemsEditor's
|
|
17
|
+
* `locked` state, but that's UI-only — this is the enforcement). Prevents a
|
|
18
|
+
* seller from swapping in a cheaper item after a buyer has already won.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertPrizeDrawWonItemsImmutable(currentProduct: Pick<ProductDocument, "prizeDrawItems">, incomingItems: unknown): void;
|
|
6
21
|
/** Assert the product has available stock for a purchase. */
|
|
7
22
|
export declare function assertInStock(product: ProductDocument, quantity?: number): void;
|
|
8
23
|
/** Derive effective price (respects currentBid for auctions, base price otherwise). */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { productRepository } from "../../../../repositories";
|
|
2
2
|
import { isAuctionListing, isPreOrderListing, } from "../../../../features/products/utils/listing-type";
|
|
3
3
|
import { ProductNotFoundError, ProductOwnershipError, ProductStatusError, ProductStockError, } from "../../../shared/features/products/errors";
|
|
4
|
+
import { ValidationError } from "../../../shared/errors/index";
|
|
4
5
|
const ALLOWED_STATUS_TRANSITIONS = {
|
|
5
6
|
draft: ["published", "archived"],
|
|
6
7
|
published: ["archived", "draft"],
|
|
@@ -25,6 +26,46 @@ export function assertStatusTransition(from, to) {
|
|
|
25
26
|
throw new ProductStatusError(from, to);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Anti-scam guard — once a prize draw has at least one paid entry, the
|
|
31
|
+
* seller can no longer take it down (unpublish/archive/delete). Prevents a
|
|
32
|
+
* seller from dodging shipment of an expensive won prize by pulling the
|
|
33
|
+
* listing after the fact.
|
|
34
|
+
*/
|
|
35
|
+
export function assertPrizeDrawNotLocked(product, action) {
|
|
36
|
+
if ((product.listingType ?? "standard") !== "prize-draw")
|
|
37
|
+
return;
|
|
38
|
+
if ((product.prizeCurrentEntries ?? 0) <= 0)
|
|
39
|
+
return;
|
|
40
|
+
throw new ValidationError(`This prize draw has active entries and cannot be ${action} — buyers have already purchased tickets. The draw must run its course.`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Anti-scam guard — once a prize item has been won, its title/images/
|
|
44
|
+
* description/condition/estimatedValue become immutable server-side (the
|
|
45
|
+
* client editor already locks this visually via PrizeDrawItemsEditor's
|
|
46
|
+
* `locked` state, but that's UI-only — this is the enforcement). Prevents a
|
|
47
|
+
* seller from swapping in a cheaper item after a buyer has already won.
|
|
48
|
+
*/
|
|
49
|
+
export function assertPrizeDrawWonItemsImmutable(currentProduct, incomingItems) {
|
|
50
|
+
if (!Array.isArray(incomingItems))
|
|
51
|
+
return;
|
|
52
|
+
const current = currentProduct.prizeDrawItems ?? [];
|
|
53
|
+
for (const currentItem of current) {
|
|
54
|
+
if (!currentItem.isWon)
|
|
55
|
+
continue;
|
|
56
|
+
const incoming = incomingItems.find((it) => it.itemNumber === currentItem.itemNumber);
|
|
57
|
+
if (!incoming)
|
|
58
|
+
continue;
|
|
59
|
+
const changed = incoming.title !== currentItem.title ||
|
|
60
|
+
incoming.description !== currentItem.description ||
|
|
61
|
+
incoming.condition !== currentItem.condition ||
|
|
62
|
+
incoming.estimatedValue !== currentItem.estimatedValue ||
|
|
63
|
+
JSON.stringify(incoming.images) !== JSON.stringify(currentItem.images);
|
|
64
|
+
if (changed) {
|
|
65
|
+
throw new ValidationError(`Prize #${currentItem.itemNumber} has already been won and can no longer be edited.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
28
69
|
/** Assert the product has available stock for a purchase. */
|
|
29
70
|
export function assertInStock(product, quantity = 1) {
|
|
30
71
|
if ((product.availableQuantity ?? 0) < quantity) {
|
|
@@ -15,6 +15,8 @@ export declare const onProductStockChange: import("./types").DocumentTriggerFunc
|
|
|
15
15
|
}, {
|
|
16
16
|
[x: string]: import("../../..").JsonValue;
|
|
17
17
|
}>;
|
|
18
|
+
export declare const onPrizeDrawPaymentConfirmed: import("./types").DocumentTriggerFunctionDefinition<import("../jobs/core/onPrizeDrawPaymentConfirmed").PrizeDrawOrderSnapshot, import("../jobs/core/onPrizeDrawPaymentConfirmed").PrizeDrawOrderSnapshot>;
|
|
19
|
+
export declare const prizeDrawSoldOutReveal: import("./types").DocumentTriggerFunctionDefinition<import("../jobs/core/prizeDrawSoldOutReveal").PrizeDrawStockSnapshot, import("../jobs/core/prizeDrawSoldOutReveal").PrizeDrawStockSnapshot>;
|
|
18
20
|
export declare const onReviewWrite: import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>;
|
|
19
21
|
export declare const onCategoryWrite: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").CategoryDoc, import("../jobs").CategoryDoc>;
|
|
20
22
|
export declare const onStoreWrite: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").StoreDoc, import("../jobs").StoreDoc>;
|
|
@@ -53,7 +55,7 @@ export declare const FIRESTORE_TRIGGER_FUNCTIONS: readonly [import("./types").Do
|
|
|
53
55
|
[x: string]: import("../../..").JsonValue;
|
|
54
56
|
}, {
|
|
55
57
|
[x: string]: import("../../..").JsonValue;
|
|
56
|
-
}>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").CategoryDoc, import("../jobs").CategoryDoc>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").StoreDoc, import("../jobs").StoreDoc>, import("./types").DocumentTriggerFunctionDefinition<null, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<null, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<{
|
|
58
|
+
}>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs/core/onPrizeDrawPaymentConfirmed").PrizeDrawOrderSnapshot, import("../jobs/core/onPrizeDrawPaymentConfirmed").PrizeDrawOrderSnapshot>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs/core/prizeDrawSoldOutReveal").PrizeDrawStockSnapshot, import("../jobs/core/prizeDrawSoldOutReveal").PrizeDrawStockSnapshot>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").CategoryDoc, import("../jobs").CategoryDoc>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").StoreDoc, import("../jobs").StoreDoc>, import("./types").DocumentTriggerFunctionDefinition<null, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<null, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<Record<string, import("../../..").JsonValue>, Record<string, import("../../..").JsonValue>>, import("./types").DocumentTriggerFunctionDefinition<{
|
|
57
59
|
[x: string]: import("../../..").JsonValue;
|
|
58
60
|
}, {
|
|
59
61
|
[x: string]: import("../../..").JsonValue;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The path patterns mirror the original `functions/src/index.ts` declarations
|
|
7
7
|
* to preserve identical deploy behavior.
|
|
8
8
|
*/
|
|
9
|
-
import { onBidPlacedHandler, onCategoryWriteHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onProductStockChangeHandler, onProductWriteHandler, onReviewWriteHandler, onScamReportCreateHandler, onScamReportUpdateHandler, onStoreWriteHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, onShipmentItemWriteHandler, onShipmentLotWriteHandler, onShipmentHeaderWriteHandler, onShipmentDeletedHandler, onCatalogueSubmittedForApprovalHandler, onJobCreatedHandler, } from "../jobs/handlers";
|
|
9
|
+
import { onBidPlacedHandler, onCategoryWriteHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onProductStockChangeHandler, onProductWriteHandler, onReviewWriteHandler, onScamReportCreateHandler, onScamReportUpdateHandler, onStoreWriteHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, onShipmentItemWriteHandler, onShipmentLotWriteHandler, onShipmentHeaderWriteHandler, onShipmentDeletedHandler, onCatalogueSubmittedForApprovalHandler, onJobCreatedHandler, onPrizeDrawPaymentConfirmedHandler, prizeDrawSoldOutRevealHandler, } from "../jobs/handlers";
|
|
10
10
|
import { defineFunction } from "./define";
|
|
11
11
|
const REGION = "asia-south1";
|
|
12
12
|
export const onBidPlaced = defineFunction({
|
|
@@ -44,6 +44,20 @@ export const onProductStockChange = defineFunction({
|
|
|
44
44
|
handler: onProductStockChangeHandler,
|
|
45
45
|
options: { region: REGION },
|
|
46
46
|
});
|
|
47
|
+
export const onPrizeDrawPaymentConfirmed = defineFunction({
|
|
48
|
+
name: "onPrizeDrawPaymentConfirmed",
|
|
49
|
+
description: "Instant-mode prize-draw winner assignment the moment an order's payment is confirmed.",
|
|
50
|
+
trigger: { kind: "documentUpdated", pathPattern: "orders/{orderId}" },
|
|
51
|
+
handler: onPrizeDrawPaymentConfirmedHandler,
|
|
52
|
+
options: { region: REGION },
|
|
53
|
+
});
|
|
54
|
+
export const prizeDrawSoldOutReveal = defineFunction({
|
|
55
|
+
name: "prizeDrawSoldOutReveal",
|
|
56
|
+
description: "Assign winners for all outstanding prize-draw orders the moment a scheduled-mode draw sells out.",
|
|
57
|
+
trigger: { kind: "documentWritten", pathPattern: "products/{productId}" },
|
|
58
|
+
handler: prizeDrawSoldOutRevealHandler,
|
|
59
|
+
options: { region: REGION },
|
|
60
|
+
});
|
|
47
61
|
export const onReviewWrite = defineFunction({
|
|
48
62
|
name: "onReviewWrite",
|
|
49
63
|
description: "Recompute product + store rating aggregates on review writes.",
|
|
@@ -155,6 +169,8 @@ export const FIRESTORE_TRIGGER_FUNCTIONS = [
|
|
|
155
169
|
onOrderStatusChange,
|
|
156
170
|
onProductWrite,
|
|
157
171
|
onProductStockChange,
|
|
172
|
+
onPrizeDrawPaymentConfirmed,
|
|
173
|
+
prizeDrawSoldOutReveal,
|
|
158
174
|
onReviewWrite,
|
|
159
175
|
onCategoryWrite,
|
|
160
176
|
onStoreWrite,
|
|
@@ -6,6 +6,7 @@ export declare const paymentReviewAutoApprove: import("./types").ScheduledFuncti
|
|
|
6
6
|
export declare const couponExpiry: import("./types").ScheduledFunctionDefinition;
|
|
7
7
|
export declare const offerExpiry: import("./types").ScheduledFunctionDefinition;
|
|
8
8
|
export declare const productStatsSync: import("./types").ScheduledFunctionDefinition;
|
|
9
|
+
export declare const revenueRollup: import("./types").ScheduledFunctionDefinition;
|
|
9
10
|
export declare const dailyDataCleanup: import("./types").ScheduledFunctionDefinition;
|
|
10
11
|
export declare const countersReconcile: import("./types").ScheduledFunctionDefinition;
|
|
11
12
|
export declare const positionsReconcile: import("./types").ScheduledFunctionDefinition;
|
|
@@ -18,11 +19,8 @@ export declare const cleanupRtdbEvents: import("./types").ScheduledFunctionDefin
|
|
|
18
19
|
export declare const mediaTmpCleanup: import("./types").ScheduledFunctionDefinition;
|
|
19
20
|
export declare const draftPrune: import("./types").ScheduledFunctionDefinition;
|
|
20
21
|
export declare const testerSandboxCleanup: import("./types").ScheduledFunctionDefinition;
|
|
21
|
-
export declare const
|
|
22
|
-
export declare const prizeRevealClose: import("./types").ScheduledFunctionDefinition;
|
|
23
|
-
export declare const prizeRevealExpiry: import("./types").ScheduledFunctionDefinition;
|
|
24
|
-
export declare const prizeRevealReminder: import("./types").ScheduledFunctionDefinition;
|
|
22
|
+
export declare const prizeDrawExpiryReveal: import("./types").ScheduledFunctionDefinition;
|
|
25
23
|
export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
|
|
26
24
|
export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
|
|
27
25
|
export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
|
|
28
|
-
export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition
|
|
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,
|
|
3
|
+
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, } from "../jobs/handlers";
|
|
4
4
|
import { defineFunction } from "./define";
|
|
5
5
|
const REGION = "asia-south1";
|
|
6
6
|
const EVERY_5_MIN = "every 5 minutes";
|
|
@@ -60,6 +60,13 @@ export const productStatsSync = defineFunction({
|
|
|
60
60
|
handler: productStatsSyncHandler,
|
|
61
61
|
options: { region: REGION, timeoutSeconds: 540, memory: "256MiB", maxInstances: 1 },
|
|
62
62
|
});
|
|
63
|
+
export const revenueRollup = defineFunction({
|
|
64
|
+
name: "revenueRollup",
|
|
65
|
+
description: "Pre-compute total delivered-order revenue into analytics/dashboardRollup, replacing an unbounded per-request scan (daily 01:30 UTC).",
|
|
66
|
+
trigger: { kind: "schedule", cron: "30 1 * * *", timeZone: "UTC" },
|
|
67
|
+
handler: revenueRollupHandler,
|
|
68
|
+
options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
|
|
69
|
+
});
|
|
63
70
|
export const dailyDataCleanup = defineFunction({
|
|
64
71
|
name: "dailyDataCleanup",
|
|
65
72
|
description: "Daily data cleanup (drafts, transient records) at 02:00 UTC.",
|
|
@@ -144,32 +151,11 @@ export const testerSandboxCleanup = defineFunction({
|
|
|
144
151
|
handler: testerSandboxCleanupHandler,
|
|
145
152
|
options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
|
|
146
153
|
});
|
|
147
|
-
export const
|
|
148
|
-
name: "
|
|
149
|
-
description: "
|
|
150
|
-
trigger: { kind: "schedule", cron: EVERY_5_MIN },
|
|
151
|
-
handler: prizeRevealOpenHandler,
|
|
152
|
-
options: { region: REGION, timeoutSeconds: 120, memory: "256MiB", maxInstances: 1 },
|
|
153
|
-
});
|
|
154
|
-
export const prizeRevealClose = defineFunction({
|
|
155
|
-
name: "prizeRevealClose",
|
|
156
|
-
description: "Flip prize-draw reveals open→closed.",
|
|
154
|
+
export const prizeDrawExpiryReveal = defineFunction({
|
|
155
|
+
name: "prizeDrawExpiryReveal",
|
|
156
|
+
description: "Assign winners for outstanding paid orders on scheduled-mode prize draws past their expiry, then close them (every 5 minutes).",
|
|
157
157
|
trigger: { kind: "schedule", cron: EVERY_5_MIN },
|
|
158
|
-
handler:
|
|
159
|
-
options: { region: REGION, timeoutSeconds: 60, memory: "256MiB", maxInstances: 1 },
|
|
160
|
-
});
|
|
161
|
-
export const prizeRevealExpiry = defineFunction({
|
|
162
|
-
name: "prizeRevealExpiry",
|
|
163
|
-
description: "Auto-refund unrevealed prize-draw entries past deadline (every 6 hours UTC).",
|
|
164
|
-
trigger: { kind: "schedule", cron: "0 */6 * * *", timeZone: "UTC" },
|
|
165
|
-
handler: prizeRevealExpiryHandler,
|
|
166
|
-
options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
|
|
167
|
-
});
|
|
168
|
-
export const prizeRevealReminder = defineFunction({
|
|
169
|
-
name: "prizeRevealReminder",
|
|
170
|
-
description: "Nudge prize-draw buyers <24h to deadline (daily 10:00 IST).",
|
|
171
|
-
trigger: { kind: "schedule", cron: "0 10 * * *", timeZone: "Asia/Kolkata" },
|
|
172
|
-
handler: prizeRevealReminderHandler,
|
|
158
|
+
handler: prizeDrawExpiryRevealHandler,
|
|
173
159
|
options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
|
|
174
160
|
});
|
|
175
161
|
export const bundleStockSync = defineFunction({
|
|
@@ -202,6 +188,7 @@ export const SCHEDULED_FUNCTIONS = [
|
|
|
202
188
|
couponExpiry,
|
|
203
189
|
offerExpiry,
|
|
204
190
|
productStatsSync,
|
|
191
|
+
revenueRollup,
|
|
205
192
|
dailyDataCleanup,
|
|
206
193
|
countersReconcile,
|
|
207
194
|
positionsReconcile,
|
|
@@ -214,10 +201,7 @@ export const SCHEDULED_FUNCTIONS = [
|
|
|
214
201
|
mediaTmpCleanup,
|
|
215
202
|
draftPrune,
|
|
216
203
|
testerSandboxCleanup,
|
|
217
|
-
|
|
218
|
-
prizeRevealClose,
|
|
219
|
-
prizeRevealExpiry,
|
|
220
|
-
prizeRevealReminder,
|
|
204
|
+
prizeDrawExpiryReveal,
|
|
221
205
|
bundleStockSync,
|
|
222
206
|
emiInstallmentReminder,
|
|
223
207
|
catalogueImageStalenessReminder,
|
|
@@ -16,9 +16,12 @@ function isMemberAvailable(data) {
|
|
|
16
16
|
return false;
|
|
17
17
|
return data.status === PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED;
|
|
18
18
|
}
|
|
19
|
-
async function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
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 {
|
|
30
|
-
export {
|
|
31
|
-
export {
|
|
32
|
-
export {
|
|
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 {
|
|
31
|
-
export {
|
|
32
|
-
export {
|
|
33
|
-
export {
|
|
30
|
+
export { assignPrizeDrawWinner } from "./prizeDrawAssignWinner";
|
|
31
|
+
export { handlePrizeDrawPaymentConfirmed } from "./onPrizeDrawPaymentConfirmed";
|
|
32
|
+
export { handlePrizeDrawSoldOut } from "./prizeDrawSoldOutReveal";
|
|
33
|
+
export { runPrizeDrawExpiryReveal } from "./prizeDrawExpiryReveal";
|
|
34
34
|
// Callables
|
|
35
35
|
export { runPromotions } from "./promotions";
|
|
36
36
|
export { runListingProcessor, supportedListingCollections, } from "./listingProcessor";
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { runWeeklyPayoutEligibility } from "./weeklyPayoutEligibility";
|
|
12
12
|
import { runHardBanCascade } from "./hardBanCascade";
|
|
13
13
|
import { runResetOtpVerification } from "./resetOtpVerification";
|
|
14
|
+
import { runWhatsAppNotify } from "./whatsappNotify";
|
|
15
|
+
import { runNewsletterExport } from "./newsletterExport";
|
|
14
16
|
async function runPayoutsWeeklyJob(_payload, ctx) {
|
|
15
17
|
const { payoutsCreated, ordersProcessed, payoutIds } = await runWeeklyPayoutEligibility(ctx);
|
|
16
18
|
return {
|
|
@@ -37,8 +39,16 @@ async function runHardBanCascadeJob(payload, ctx) {
|
|
|
37
39
|
async function runResetOtpVerificationJob(_payload, ctx) {
|
|
38
40
|
return runResetOtpVerification(ctx);
|
|
39
41
|
}
|
|
42
|
+
async function runWhatsAppNotifyJob(payload, ctx) {
|
|
43
|
+
return runWhatsAppNotify(payload, ctx);
|
|
44
|
+
}
|
|
45
|
+
async function runNewsletterExportJob(_payload, ctx) {
|
|
46
|
+
return runNewsletterExport(ctx);
|
|
47
|
+
}
|
|
40
48
|
export const JOB_RUNNERS = {
|
|
41
49
|
payoutsWeekly: runPayoutsWeeklyJob,
|
|
42
50
|
hardBanCascade: runHardBanCascadeJob,
|
|
43
51
|
resetOtpVerification: runResetOtpVerificationJob,
|
|
52
|
+
whatsappNotify: runWhatsAppNotifyJob,
|
|
53
|
+
newsletterExport: runNewsletterExportJob,
|
|
44
54
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: async newsletter-subscriber CSV export (`newsletterExport` job).
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/admin/newsletter/export` used to build the full CSV in-process,
|
|
5
|
+
* synchronously, for up to 10,000 subscribers — a real 10s-timeout risk on
|
|
6
|
+
* Vercel Hobby as the list grows (CLAUDE.md Rule #6). This runner does the
|
|
7
|
+
* same work inside a Firebase Function instead and writes the CSV text onto
|
|
8
|
+
* the job's `result.data.csv` field — small enough for a Firestore doc at
|
|
9
|
+
* realistic subscriber counts, and avoids routing through Storage (which
|
|
10
|
+
* would need a `/media/...` proxy URL per the Media Architecture rules
|
|
11
|
+
* rather than a raw signed URL).
|
|
12
|
+
*/
|
|
13
|
+
import type { JobContext } from "../runtime/types";
|
|
14
|
+
import type { JobRunResult } from "./jobRunners";
|
|
15
|
+
export declare function runNewsletterExport(ctx: JobContext): Promise<JobRunResult>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: async newsletter-subscriber CSV export (`newsletterExport` job).
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/admin/newsletter/export` used to build the full CSV in-process,
|
|
5
|
+
* synchronously, for up to 10,000 subscribers — a real 10s-timeout risk on
|
|
6
|
+
* Vercel Hobby as the list grows (CLAUDE.md Rule #6). This runner does the
|
|
7
|
+
* same work inside a Firebase Function instead and writes the CSV text onto
|
|
8
|
+
* the job's `result.data.csv` field — small enough for a Firestore doc at
|
|
9
|
+
* realistic subscriber counts, and avoids routing through Storage (which
|
|
10
|
+
* would need a `/media/...` proxy URL per the Media Architecture rules
|
|
11
|
+
* rather than a raw signed URL).
|
|
12
|
+
*/
|
|
13
|
+
import { newsletterRepository } from "../../../../repositories";
|
|
14
|
+
import { sortBy } from "../../../../constants/sort";
|
|
15
|
+
import { COMMON_FIELDS } from "../../../../constants/field-names";
|
|
16
|
+
function escape(value) {
|
|
17
|
+
const s = String(value ?? "");
|
|
18
|
+
return s.includes(",") || s.includes('"') || s.includes("\n")
|
|
19
|
+
? `"${s.replace(/"/g, '""')}"`
|
|
20
|
+
: s;
|
|
21
|
+
}
|
|
22
|
+
function csvRow(cols) {
|
|
23
|
+
return cols.map(escape).join(",");
|
|
24
|
+
}
|
|
25
|
+
export async function runNewsletterExport(ctx) {
|
|
26
|
+
const result = await newsletterRepository.list({
|
|
27
|
+
sorts: sortBy(COMMON_FIELDS.CREATED_AT),
|
|
28
|
+
page: "1",
|
|
29
|
+
pageSize: "10000",
|
|
30
|
+
});
|
|
31
|
+
const rows = result.data;
|
|
32
|
+
const header = csvRow(["id", "email", "status", "source", "subscribedAt", "createdAt"]);
|
|
33
|
+
const dataRows = rows.map((s) => csvRow([s["id"], s["email"] ?? "", s["status"] ?? "", s["source"] ?? "", s["subscribedAt"] ?? "", s["createdAt"] ?? ""]));
|
|
34
|
+
const csv = [header, ...dataRows].join("\r\n");
|
|
35
|
+
ctx.logger.info("newsletterExport: complete", { subscriberCount: rows.length });
|
|
36
|
+
return {
|
|
37
|
+
summary: { total: rows.length, succeeded: rows.length, skipped: 0, failed: 0 },
|
|
38
|
+
succeeded: [],
|
|
39
|
+
skipped: [],
|
|
40
|
+
failed: [],
|
|
41
|
+
data: { csv, subscriberCount: rows.length },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -6,6 +6,8 @@ export type OrderAfter = {
|
|
|
6
6
|
userEmail: string;
|
|
7
7
|
productTitle: string;
|
|
8
8
|
trackingNumber?: string;
|
|
9
|
+
/** Buyer opted into the ₹10 WhatsApp order-updates addon at checkout. */
|
|
10
|
+
whatsappNotifyAddon?: boolean;
|
|
9
11
|
};
|
|
10
12
|
export type OrderBefore = {
|
|
11
13
|
status: OrderStatus;
|
|
@@ -56,6 +56,7 @@ export async function handleOrderStatusChange(input, ctx) {
|
|
|
56
56
|
relatedId: orderId,
|
|
57
57
|
relatedType: "order",
|
|
58
58
|
userEmail,
|
|
59
|
+
orderWhatsappAddonPaid: after.whatsappNotifyAddon === true,
|
|
59
60
|
});
|
|
60
61
|
try {
|
|
61
62
|
await getAdminRealtimeDb().ref(`notifications/${after.userId}`).push({
|