@mohasinac/appkit 2.6.1 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +38 -0
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +75 -0
- package/dist/_internal/client/features/layout/RoleGuard.d.ts +15 -0
- package/dist/_internal/client/features/layout/RoleGuard.js +25 -0
- package/dist/_internal/client/features/layout/index.d.ts +5 -0
- package/dist/_internal/client/features/layout/index.js +4 -0
- package/dist/_internal/server/features/brands/actions.d.ts +3 -3
- package/dist/_internal/server/features/brands/actions.js +72 -5
- package/dist/_internal/server/features/brands/data.d.ts +8 -8
- package/dist/_internal/server/features/brands/data.js +10 -11
- package/dist/_internal/server/features/brands/service.d.ts +2 -2
- package/dist/_internal/server/features/brands/service.js +5 -5
- package/dist/_internal/server/features/categories/og.d.ts +33 -0
- package/dist/_internal/server/features/categories/og.js +75 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +24 -0
- package/dist/_internal/server/features/checkout/actions.js +442 -13
- package/dist/_internal/server/features/checkout/index.d.ts +1 -1
- package/dist/_internal/server/features/checkout/index.js +1 -1
- package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +59 -0
- package/dist/_internal/server/features/checkout/prize-bundle-gates.js +99 -0
- package/dist/_internal/server/features/grouped/data.js +12 -5
- package/dist/_internal/server/features/homepage/data.d.ts +1 -1
- package/dist/_internal/server/features/homepage/data.js +2 -2
- package/dist/_internal/server/features/media/contextGuards.d.ts +52 -0
- package/dist/_internal/server/features/media/contextGuards.js +198 -0
- package/dist/_internal/server/features/orders/adapters.js +12 -0
- package/dist/_internal/server/features/products/data.d.ts +1 -1
- package/dist/_internal/server/features/sublisting-categories/data.d.ts +1 -1
- package/dist/_internal/server/features/sublisting-categories/data.js +2 -2
- package/dist/_internal/server/jobs/handlers/assignSpinPrize.d.ts +24 -0
- package/dist/_internal/server/jobs/handlers/assignSpinPrize.js +86 -0
- package/dist/_internal/server/jobs/handlers/bundleStockSync.d.ts +18 -0
- package/dist/_internal/server/jobs/handlers/bundleStockSync.js +80 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +8 -0
- package/dist/_internal/server/jobs/handlers/index.js +13 -0
- package/dist/_internal/server/jobs/handlers/listingProcessor.js +13 -3
- package/dist/_internal/server/jobs/handlers/onProductStockChange.d.ts +17 -0
- package/dist/_internal/server/jobs/handlers/onProductStockChange.js +136 -0
- package/dist/_internal/server/jobs/handlers/onProductWrite.js +17 -1
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.d.ts +9 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.js +29 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.d.ts +10 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.js +58 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.d.ts +10 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.js +65 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.d.ts +9 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.js +45 -0
- package/dist/_internal/server/jobs/handlers/triggerEventRaffle.d.ts +30 -0
- package/dist/_internal/server/jobs/handlers/triggerEventRaffle.js +94 -0
- package/dist/_internal/shared/features/brands/schema.d.ts +3 -3
- package/dist/_internal/shared/features/cart/schema.d.ts +8 -8
- package/dist/_internal/shared/features/cart/schema.js +1 -1
- package/dist/_internal/shared/features/categories/bundle-config.d.ts +17 -0
- package/dist/_internal/shared/features/categories/bundle-config.js +17 -0
- package/dist/_internal/shared/features/layout/config.d.ts +35 -0
- package/dist/_internal/shared/features/layout/config.js +58 -0
- package/dist/_internal/shared/features/layout/index.d.ts +3 -0
- package/dist/_internal/shared/features/layout/index.js +2 -0
- package/dist/_internal/shared/features/layout/types.d.ts +137 -0
- package/dist/_internal/shared/features/layout/types.js +13 -0
- package/dist/_internal/shared/features/products/types.d.ts +1 -1
- package/dist/_internal/shared/listing-types/_registry.d.ts +57 -0
- package/dist/_internal/shared/listing-types/_registry.js +28 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +7 -0
- package/dist/_internal/shared/listing-types/auction/config.js +8 -0
- package/dist/_internal/shared/listing-types/auction/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/ctas.js +2 -0
- package/dist/_internal/shared/listing-types/auction/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/og.js +1 -0
- package/dist/_internal/shared/listing-types/auction/schema.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/schema.js +1 -0
- package/dist/_internal/shared/listing-types/auction/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/capabilities.d.ts +41 -0
- package/dist/_internal/shared/listing-types/capabilities.js +75 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +7 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +8 -0
- package/dist/_internal/shared/listing-types/pre-order/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/ctas.js +2 -0
- package/dist/_internal/shared/listing-types/pre-order/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/og.js +1 -0
- package/dist/_internal/shared/listing-types/pre-order/schema.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/schema.js +1 -0
- package/dist/_internal/shared/listing-types/pre-order/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +7 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +8 -0
- package/dist/_internal/shared/listing-types/prize-draw/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/ctas.js +2 -0
- package/dist/_internal/shared/listing-types/prize-draw/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/og.js +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/schema.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/schema.js +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +7 -0
- package/dist/_internal/shared/listing-types/standard/config.js +8 -0
- package/dist/_internal/shared/listing-types/standard/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/ctas.js +3 -0
- package/dist/_internal/shared/listing-types/standard/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/og.js +1 -0
- package/dist/_internal/shared/listing-types/standard/schema.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/schema.js +1 -0
- package/dist/_internal/shared/listing-types/standard/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/seed-factory.js +1 -0
- package/dist/_internal/shared/media/limits.d.ts +33 -0
- package/dist/_internal/shared/media/limits.js +97 -0
- package/dist/_internal/shared/schema-versions.d.ts +76 -0
- package/dist/_internal/shared/schema-versions.js +82 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +7 -0
- package/dist/constants/api-endpoints.d.ts +6 -3
- package/dist/constants/api-endpoints.js +2 -1
- package/dist/errors/messages.d.ts +1 -1
- package/dist/errors/messages.js +1 -1
- package/dist/features/account/migrations.d.ts +2 -0
- package/dist/features/account/migrations.js +10 -0
- package/dist/features/admin/components/AdminMediaView.js +1 -1
- package/dist/features/admin/components/AdminProductsView.js +7 -3
- package/dist/features/admin/migrations.d.ts +2 -0
- package/dist/features/admin/migrations.js +10 -0
- package/dist/features/admin/types/product.types.d.ts +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +1 -1
- package/dist/features/auctions/migrations.d.ts +2 -0
- package/dist/features/auctions/migrations.js +10 -0
- package/dist/features/auctions/schemas/index.d.ts +3 -3
- package/dist/features/auctions/schemas/index.js +1 -1
- package/dist/features/auth/migrations.d.ts +2 -0
- package/dist/features/auth/migrations.js +10 -0
- package/dist/features/blog/migrations.d.ts +2 -0
- package/dist/features/blog/migrations.js +10 -0
- package/dist/features/brands/migrations.d.ts +2 -0
- package/dist/features/brands/migrations.js +10 -0
- package/dist/features/bundles/components/BundlesByCategoryListing.d.ts +6 -0
- package/dist/features/bundles/components/BundlesByCategoryListing.js +50 -0
- package/dist/features/bundles/components/index.d.ts +2 -0
- package/dist/features/bundles/components/index.js +1 -0
- package/dist/features/bundles/migrations.d.ts +2 -0
- package/dist/features/bundles/migrations.js +10 -0
- package/dist/features/bundles/schemas/index.d.ts +1 -0
- package/dist/features/bundles/schemas/index.js +1 -0
- package/dist/features/bundles/schemas/zod.d.ts +377 -0
- package/dist/features/bundles/schemas/zod.js +71 -0
- package/dist/features/cart/migrations.d.ts +2 -0
- package/dist/features/cart/migrations.js +10 -0
- package/dist/features/cart/schemas/firestore.d.ts +2 -2
- package/dist/features/categories/components/BrandDetailPageView.js +35 -4
- package/dist/features/categories/components/BrandDetailTabs.d.ts +5 -1
- package/dist/features/categories/components/BrandDetailTabs.js +22 -8
- package/dist/features/categories/components/CategoryBundlesListing.d.ts +6 -0
- package/dist/features/categories/components/CategoryBundlesListing.js +74 -0
- package/dist/features/categories/components/CategoryDetailPageView.js +29 -4
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +22 -8
- package/dist/features/categories/migrations.d.ts +2 -0
- package/dist/features/categories/migrations.js +10 -0
- package/dist/features/categories/repository/categories.repository.d.ts +29 -0
- package/dist/features/categories/repository/categories.repository.js +83 -0
- package/dist/features/categories/schemas/firestore.d.ts +59 -2
- package/dist/features/categories/schemas/firestore.js +6 -0
- package/dist/features/categories/types/index.d.ts +11 -3
- package/dist/features/events/migrations.d.ts +2 -0
- package/dist/features/events/migrations.js +10 -0
- package/dist/features/faq/migrations.d.ts +2 -0
- package/dist/features/faq/migrations.js +10 -0
- package/dist/features/grouped/migrations.d.ts +2 -0
- package/dist/features/grouped/migrations.js +10 -0
- package/dist/features/grouped/schemas/firestore.d.ts +29 -10
- package/dist/features/grouped/schemas/firestore.js +10 -5
- package/dist/features/history/migrations.d.ts +2 -0
- package/dist/features/history/migrations.js +10 -0
- package/dist/features/homepage/hooks/useFeaturedAuctions.js +2 -2
- package/dist/features/homepage/hooks/useFeaturedPreOrders.js +2 -2
- package/dist/features/homepage/lib/section-renderer.js +5 -3
- package/dist/features/media/AvatarUpload.js +6 -28
- package/dist/features/media/hooks/useMedia.d.ts +31 -15
- package/dist/features/media/hooks/useMedia.js +48 -13
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/media/upload/MediaUploadField.js +1 -1
- package/dist/features/messages/migrations.d.ts +2 -0
- package/dist/features/messages/migrations.js +10 -0
- package/dist/features/orders/components/OrdersList.js +10 -1
- package/dist/features/orders/migrations.d.ts +2 -0
- package/dist/features/orders/migrations.js +10 -0
- package/dist/features/orders/repository/orders.repository.d.ts +16 -0
- package/dist/features/orders/repository/orders.repository.js +49 -0
- package/dist/features/orders/schemas/firestore.d.ts +8 -0
- package/dist/features/orders/types/index.d.ts +12 -0
- package/dist/features/orders/utils/order-splitter.d.ts +2 -2
- package/dist/features/orders/utils/order-splitter.js +5 -0
- package/dist/features/payments/migrations.d.ts +2 -0
- package/dist/features/payments/migrations.js +10 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
- package/dist/features/products/actions/product-actions.d.ts +1 -1
- package/dist/features/products/api/[id]/route.js +34 -0
- package/dist/features/products/api/route.js +1 -19
- package/dist/features/products/components/CompareOverlay.d.ts +1 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +24 -0
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +102 -0
- package/dist/features/products/components/PrizeDrawCollage.d.ts +32 -0
- package/dist/features/products/components/PrizeDrawCollage.js +22 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.d.ts +27 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.js +118 -0
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +19 -0
- package/dist/features/products/components/PrizeDrawEntryActions.js +48 -0
- package/dist/features/products/components/PrizeDrawItemsEditor.d.ts +13 -0
- package/dist/features/products/components/PrizeDrawItemsEditor.js +97 -0
- package/dist/features/products/components/PrizeDrawsIndexListing.d.ts +8 -0
- package/dist/features/products/components/PrizeDrawsIndexListing.js +128 -0
- package/dist/features/products/components/PrizeDrawsListingView.d.ts +15 -0
- package/dist/features/products/components/PrizeDrawsListingView.js +49 -0
- package/dist/features/products/components/PrizeRevealModal.d.ts +34 -0
- package/dist/features/products/components/PrizeRevealModal.js +124 -0
- package/dist/features/products/components/ProductDetailPageView.js +13 -1
- package/dist/features/products/components/ProductForm.js +35 -2
- package/dist/features/products/components/ProductGrid.js +3 -1
- package/dist/features/products/components/index.d.ts +16 -0
- package/dist/features/products/components/index.js +8 -0
- package/dist/features/products/constants/listing-tabs.d.ts +113 -0
- package/dist/features/products/constants/listing-tabs.js +43 -0
- package/dist/features/products/index.d.ts +1 -0
- package/dist/features/products/index.js +1 -0
- package/dist/features/products/migrations.d.ts +2 -0
- package/dist/features/products/migrations.js +10 -0
- package/dist/features/products/repository/products.repository.d.ts +11 -7
- package/dist/features/products/repository/products.repository.js +49 -24
- package/dist/features/products/schemas/firestore.d.ts +3 -3
- package/dist/features/products/schemas/firestore.js +2 -2
- package/dist/features/products/schemas/index.d.ts +5 -5
- package/dist/features/products/schemas/index.js +3 -1
- package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
- package/dist/features/products/types/index.d.ts +17 -1
- package/dist/features/products/utils/listing-type.d.ts +7 -4
- package/dist/features/products/utils/listing-type.js +8 -4
- package/dist/features/promotions/actions/coupon-actions.d.ts +1 -1
- package/dist/features/promotions/hooks/useCouponValidate.d.ts +1 -1
- package/dist/features/promotions/migrations.d.ts +2 -0
- package/dist/features/promotions/migrations.js +10 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +1 -1
- package/dist/features/promotions/schemas/index.d.ts +2 -2
- package/dist/features/reviews/migrations.d.ts +2 -0
- package/dist/features/reviews/migrations.js +10 -0
- package/dist/features/scams/migrations.d.ts +2 -0
- package/dist/features/scams/migrations.js +10 -0
- package/dist/features/search/api/route.d.ts +1 -1
- package/dist/features/search/api/route.js +3 -3
- package/dist/features/search/components/Search.d.ts +1 -1
- package/dist/features/search/schemas/index.d.ts +3 -3
- package/dist/features/search/schemas/index.js +3 -1
- package/dist/features/search/types/index.d.ts +2 -2
- package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
- package/dist/features/seller/components/SellerProductsView.js +20 -6
- package/dist/features/seller/migrations.d.ts +2 -0
- package/dist/features/seller/migrations.js +10 -0
- package/dist/features/seller/schemas/index.d.ts +2 -2
- package/dist/features/stores/components/StoreBundlesPageView.d.ts +12 -0
- package/dist/features/stores/components/StoreBundlesPageView.js +24 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +15 -3
- package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +11 -0
- package/dist/features/stores/components/StorePrizeDrawsPageView.js +27 -0
- package/dist/features/stores/components/index.d.ts +2 -0
- package/dist/features/stores/migrations.d.ts +2 -0
- package/dist/features/stores/migrations.js +10 -0
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/stores/types/index.d.ts +1 -1
- package/dist/features/sublisting/migrations.d.ts +2 -0
- package/dist/features/sublisting/migrations.js +10 -0
- package/dist/features/sublisting/schemas/firestore.d.ts +2 -0
- package/dist/features/support/migrations.d.ts +2 -0
- package/dist/features/support/migrations.js +10 -0
- package/dist/features/wishlist/migrations.d.ts +2 -0
- package/dist/features/wishlist/migrations.js +10 -0
- package/dist/features/wishlist/types/index.d.ts +1 -1
- package/dist/index.d.ts +26 -18
- package/dist/index.js +41 -24
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +4 -0
- package/dist/next/routing/route-map.d.ts +4 -0
- package/dist/next/routing/route-map.js +2 -0
- package/dist/providers/db-firebase/filter-aliases.d.ts +2 -2
- package/dist/repositories/index.d.ts +0 -5
- package/dist/repositories/index.js +5 -4
- package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
- package/dist/seed/categories-seed-data.js +1105 -6
- package/dist/seed/faq-seed-data.js +160 -0
- package/dist/seed/grouped-listings-seed-data.js +32 -32
- package/dist/seed/homepage-sections-seed-data.js +52 -6
- package/dist/seed/index.d.ts +1 -3
- package/dist/seed/index.js +4 -3
- package/dist/seed/manifest.js +8 -13
- package/dist/seed/products-prize-draws-seed-data.d.ts +17 -0
- package/dist/seed/products-prize-draws-seed-data.js +313 -0
- package/dist/seo/json-ld.d.ts +1 -1
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +5 -3
- package/dist/server.d.ts +9 -2
- package/dist/server.js +11 -5
- package/dist/tailwind-utilities.css +1 -1
- package/dist/validation/schemas.d.ts +8 -8
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +2 -4
|
@@ -33,3 +33,16 @@ export { onStoreWriteHandler } from "./onStoreWrite";
|
|
|
33
33
|
export { adminAnalyticsHandler } from "./adminAnalytics";
|
|
34
34
|
export { storeAnalyticsHandler, } from "./storeAnalytics";
|
|
35
35
|
export { listingProcessorHandler, supportedListingCollections, } from "./listingProcessor";
|
|
36
|
+
// SB1-L (S7-PrizeDraws) — 7 functions covering prize draws, bundle sync,
|
|
37
|
+
// event raffles, and spin-the-wheel.
|
|
38
|
+
export { prizeRevealOpenHandler } from "./prizeRevealOpen";
|
|
39
|
+
export { prizeRevealCloseHandler } from "./prizeRevealClose";
|
|
40
|
+
export { prizeRevealExpiryHandler } from "./prizeRevealExpiry";
|
|
41
|
+
export { prizeRevealReminderHandler } from "./prizeRevealReminder";
|
|
42
|
+
export { bundleStockSyncHandler } from "./bundleStockSync";
|
|
43
|
+
// SB-UNI-V — Firestore onWrite trigger for product stock changes (recomputes
|
|
44
|
+
// bundleStockStatus on categoryType:"bundle" rows + activeMemberCount on
|
|
45
|
+
// groupedListings).
|
|
46
|
+
export { onProductStockChangeHandler } from "./onProductStockChange";
|
|
47
|
+
export { triggerEventRaffleHandler } from "./triggerEventRaffle";
|
|
48
|
+
export { assignSpinPrizeHandler } from "./assignSpinPrize";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Cursor: opaque base64 over `{ page }`. Callers may use either page-mode
|
|
7
7
|
* (`p` directly) or infinite-mode (forward `cursor` from previous response).
|
|
8
8
|
*/
|
|
9
|
-
import { bidRepository, blogRepository,
|
|
9
|
+
import { bidRepository, blogRepository, categoriesRepository, couponsRepository, eventEntryRepository, eventRepository, faqsRepository, homepageSectionsRepository, notificationRepository, orderRepository, payoutRepository, productFeaturesRepository, productRepository, productTemplateRepository, reviewRepository, scammerRepository, storeRepository, userRepository, } from "../../../../repositories";
|
|
10
10
|
const DEFAULT_SORT = "-createdAt";
|
|
11
11
|
const DEFAULT_PAGE = 1;
|
|
12
12
|
const DEFAULT_PAGE_SIZE = 20;
|
|
@@ -38,7 +38,12 @@ function getBoolOpt(baseOpts, key, fallback) {
|
|
|
38
38
|
const LISTERS = {
|
|
39
39
|
products: (m, o) => productRepository.list(m, o),
|
|
40
40
|
categories: (m) => categoriesRepository.list(m),
|
|
41
|
-
brands: (m) =>
|
|
41
|
+
brands: (m) => categoriesRepository.list({
|
|
42
|
+
...m,
|
|
43
|
+
filters: m.filters
|
|
44
|
+
? `${m.filters},categoryType==brand`
|
|
45
|
+
: "categoryType==brand",
|
|
46
|
+
}),
|
|
42
47
|
orders: (m) => orderRepository.listAll(m),
|
|
43
48
|
reviews: (m) => reviewRepository.listAll(m),
|
|
44
49
|
coupons: (m) => couponsRepository.list(m),
|
|
@@ -49,7 +54,12 @@ const LISTERS = {
|
|
|
49
54
|
faqs: (m) => faqsRepository.list(m),
|
|
50
55
|
notifications: (m) => notificationRepository.list(m),
|
|
51
56
|
scammers: (m) => scammerRepository.listAll(m),
|
|
52
|
-
sublistingCategories: (m) =>
|
|
57
|
+
sublistingCategories: (m) => categoriesRepository.list({
|
|
58
|
+
...m,
|
|
59
|
+
filters: m.filters
|
|
60
|
+
? `${m.filters},categoryType==sublisting`
|
|
61
|
+
: "categoryType==sublisting",
|
|
62
|
+
}),
|
|
53
63
|
productFeatures: (m) => productFeaturesRepository.list(m),
|
|
54
64
|
homepageSections: (m) => homepageSectionsRepository.list(m),
|
|
55
65
|
users: (m) => userRepository.list(m),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* onProductStockChange — SB-UNI-V Firestore onWrite trigger on products.
|
|
3
|
+
*
|
|
4
|
+
* Fires when any product document is created / updated / deleted. Looks
|
|
5
|
+
* up every bundle category (categoryType:"bundle") that lists the product
|
|
6
|
+
* in `bundleProductIds[]` and recomputes its `bundleStockStatus`. Also
|
|
7
|
+
* updates every grouped listing that lists the product in `productIds[]`
|
|
8
|
+
* — bumps `activeMemberCount` and `visibilityStatus`.
|
|
9
|
+
*
|
|
10
|
+
* Cheap: each affected bundle/group reads its `bundleProductIds.length`
|
|
11
|
+
* member products (≤16 typically). Skips entirely when the product's
|
|
12
|
+
* status didn't transition between "available" and "unavailable".
|
|
13
|
+
*/
|
|
14
|
+
import type { FirestoreTriggerHandler } from "../runtime/types";
|
|
15
|
+
type ProductDoc = Record<string, unknown>;
|
|
16
|
+
export declare const onProductStockChangeHandler: FirestoreTriggerHandler<ProductDoc, ProductDoc>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* onProductStockChange — SB-UNI-V Firestore onWrite trigger on products.
|
|
3
|
+
*
|
|
4
|
+
* Fires when any product document is created / updated / deleted. Looks
|
|
5
|
+
* up every bundle category (categoryType:"bundle") that lists the product
|
|
6
|
+
* in `bundleProductIds[]` and recomputes its `bundleStockStatus`. Also
|
|
7
|
+
* updates every grouped listing that lists the product in `productIds[]`
|
|
8
|
+
* — bumps `activeMemberCount` and `visibilityStatus`.
|
|
9
|
+
*
|
|
10
|
+
* Cheap: each affected bundle/group reads its `bundleProductIds.length`
|
|
11
|
+
* member products (≤16 typically). Skips entirely when the product's
|
|
12
|
+
* status didn't transition between "available" and "unavailable".
|
|
13
|
+
*/
|
|
14
|
+
const CATEGORIES_COLLECTION = "categories";
|
|
15
|
+
const GROUPED_LISTINGS_COLLECTION = "groupedListings";
|
|
16
|
+
const PRODUCT_COLLECTION = "products";
|
|
17
|
+
const UNAVAILABLE_STATUSES = new Set(["sold", "out_of_stock", "discontinued"]);
|
|
18
|
+
const MIN_ACTIVE_DEFAULT = 2;
|
|
19
|
+
function isAvailable(status) {
|
|
20
|
+
if (!status)
|
|
21
|
+
return false;
|
|
22
|
+
return !UNAVAILABLE_STATUSES.has(status);
|
|
23
|
+
}
|
|
24
|
+
async function recomputeBundleStatus(productIds, minActive, ctx) {
|
|
25
|
+
if (productIds.length === 0)
|
|
26
|
+
return "out_of_stock";
|
|
27
|
+
let unavailable = 0;
|
|
28
|
+
for (let i = 0; i < productIds.length; i += 30) {
|
|
29
|
+
const chunk = productIds.slice(i, i + 30);
|
|
30
|
+
const snap = await ctx.db
|
|
31
|
+
.collection(PRODUCT_COLLECTION)
|
|
32
|
+
.where("__name__", "in", chunk)
|
|
33
|
+
.get();
|
|
34
|
+
for (const doc of snap.docs) {
|
|
35
|
+
const status = doc.data().status;
|
|
36
|
+
if (!isAvailable(status))
|
|
37
|
+
unavailable++;
|
|
38
|
+
}
|
|
39
|
+
if (snap.size < chunk.length)
|
|
40
|
+
unavailable += chunk.length - snap.size;
|
|
41
|
+
}
|
|
42
|
+
const active = productIds.length - unavailable;
|
|
43
|
+
if (unavailable === 0)
|
|
44
|
+
return "in_stock";
|
|
45
|
+
if (active < minActive)
|
|
46
|
+
return "out_of_stock";
|
|
47
|
+
return "partial";
|
|
48
|
+
}
|
|
49
|
+
async function syncBundlesForProduct(productId, ctx) {
|
|
50
|
+
const snap = await ctx.db
|
|
51
|
+
.collection(CATEGORIES_COLLECTION)
|
|
52
|
+
.where("categoryType", "==", "bundle")
|
|
53
|
+
.where("bundleProductIds", "array-contains", productId)
|
|
54
|
+
.get();
|
|
55
|
+
let updated = 0;
|
|
56
|
+
for (const bundleDoc of snap.docs) {
|
|
57
|
+
const data = bundleDoc.data();
|
|
58
|
+
const ids = data.bundleProductIds ?? [];
|
|
59
|
+
const minActive = data.minActiveMembers ?? 1;
|
|
60
|
+
const next = await recomputeBundleStatus(ids, minActive, ctx);
|
|
61
|
+
if (next !== data.bundleStockStatus) {
|
|
62
|
+
await bundleDoc.ref.update({
|
|
63
|
+
bundleStockStatus: next,
|
|
64
|
+
bundleQueryResolvedAt: ctx.now,
|
|
65
|
+
updatedAt: ctx.now,
|
|
66
|
+
});
|
|
67
|
+
updated++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return updated;
|
|
71
|
+
}
|
|
72
|
+
async function syncGroupedListingsForProduct(productId, ctx) {
|
|
73
|
+
const snap = await ctx.db
|
|
74
|
+
.collection(GROUPED_LISTINGS_COLLECTION)
|
|
75
|
+
.where("productIds", "array-contains", productId)
|
|
76
|
+
.get();
|
|
77
|
+
let updated = 0;
|
|
78
|
+
for (const groupDoc of snap.docs) {
|
|
79
|
+
const data = groupDoc.data();
|
|
80
|
+
const productIds = data.productIds ?? [];
|
|
81
|
+
if (productIds.length === 0)
|
|
82
|
+
continue;
|
|
83
|
+
const minActive = data.minActiveMembers ?? MIN_ACTIVE_DEFAULT;
|
|
84
|
+
let activeCount = 0;
|
|
85
|
+
for (let i = 0; i < productIds.length; i += 30) {
|
|
86
|
+
const chunk = productIds.slice(i, i + 30);
|
|
87
|
+
const productSnap = await ctx.db
|
|
88
|
+
.collection(PRODUCT_COLLECTION)
|
|
89
|
+
.where("__name__", "in", chunk)
|
|
90
|
+
.get();
|
|
91
|
+
for (const doc of productSnap.docs) {
|
|
92
|
+
const status = doc.data().status;
|
|
93
|
+
if (isAvailable(status))
|
|
94
|
+
activeCount++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const nextVisibility = activeCount >= minActive ? "visible" : "hidden";
|
|
98
|
+
if (activeCount !== data.activeMemberCount ||
|
|
99
|
+
nextVisibility !== data.visibilityStatus) {
|
|
100
|
+
await groupDoc.ref.update({
|
|
101
|
+
activeMemberCount: activeCount,
|
|
102
|
+
visibilityStatus: nextVisibility,
|
|
103
|
+
updatedAt: ctx.now,
|
|
104
|
+
});
|
|
105
|
+
updated++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return updated;
|
|
109
|
+
}
|
|
110
|
+
export const onProductStockChangeHandler = async (event, ctx) => {
|
|
111
|
+
const productId = event.params.productId;
|
|
112
|
+
const before = event.before;
|
|
113
|
+
const after = event.after;
|
|
114
|
+
// Only react when the available/unavailable state actually flipped.
|
|
115
|
+
// Saves Firestore reads on routine writes (price edits, image updates, etc.).
|
|
116
|
+
const beforeAvailable = isAvailable(before?.status);
|
|
117
|
+
const afterAvailable = isAvailable(after?.status);
|
|
118
|
+
const isDelete = !after;
|
|
119
|
+
const stateFlipped = beforeAvailable !== afterAvailable;
|
|
120
|
+
if (!isDelete && !stateFlipped) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const bundlesUpdated = await syncBundlesForProduct(productId, ctx);
|
|
125
|
+
const groupsUpdated = await syncGroupedListingsForProduct(productId, ctx);
|
|
126
|
+
ctx.logger.info("onProductStockChange synced", {
|
|
127
|
+
productId,
|
|
128
|
+
bundlesUpdated,
|
|
129
|
+
groupsUpdated,
|
|
130
|
+
isDelete,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
ctx.logger.error("onProductStockChange sync failed (non-fatal)", err, { productId });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
@@ -2,8 +2,21 @@
|
|
|
2
2
|
* Trigger handler: keep category metrics + store stats in sync when a
|
|
3
3
|
* product document is written. Only published products count.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
// SB-UNI-V — bundlesRepository deleted; bundle stock-sync moves to the
|
|
6
|
+
// dedicated `onProductStockChange` Firebase Function handler.
|
|
7
|
+
import { categoriesRepository, storeRepository, } from "../../../../repositories";
|
|
6
8
|
import { ProductStatusValues } from "../../../../features/products/schemas/firestore";
|
|
9
|
+
const UNAVAILABLE_PRODUCT_STATUSES = new Set([
|
|
10
|
+
ProductStatusValues.SOLD,
|
|
11
|
+
ProductStatusValues.OUT_OF_STOCK,
|
|
12
|
+
ProductStatusValues.DISCONTINUED,
|
|
13
|
+
]);
|
|
14
|
+
// SB-UNI-V — `syncBundlesOnUnavailableTransition` deleted. The
|
|
15
|
+
// `onProductStockChange` Firebase Function (registered separately) now
|
|
16
|
+
// observes product writes and recomputes each bundle category's
|
|
17
|
+
// `bundleStockStatus` via `partOfBundleCategoryIds[]` on the product.
|
|
18
|
+
// UNAVAILABLE_PRODUCT_STATUSES retained — referenced by the function.
|
|
19
|
+
void UNAVAILABLE_PRODUCT_STATUSES;
|
|
7
20
|
async function getParentIds(categoryId) {
|
|
8
21
|
if (!categoryId)
|
|
9
22
|
return [];
|
|
@@ -89,4 +102,7 @@ export const onProductWriteHandler = async (event, ctx) => {
|
|
|
89
102
|
catch (err) {
|
|
90
103
|
ctx.logger.error("Counter update failed (non-fatal)", err, { productId });
|
|
91
104
|
}
|
|
105
|
+
// SB-UNI-V — bundle stock-sync moved to onProductStockChange Function.
|
|
106
|
+
void before;
|
|
107
|
+
void after;
|
|
92
108
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduledPrizeRevealClose — every 5 minutes.
|
|
3
|
+
*
|
|
4
|
+
* Flips `prizeRevealStatus` from "open" to "closed" on any prize-draw
|
|
5
|
+
* product whose `prizeRevealWindowEnd` has passed. Pair function to
|
|
6
|
+
* `prizeRevealOpen`. Orders past the deadline are handled by `prizeRevealExpiry`.
|
|
7
|
+
*/
|
|
8
|
+
import type { ScheduleHandler } from "../runtime/types";
|
|
9
|
+
export declare const prizeRevealCloseHandler: ScheduleHandler;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduledPrizeRevealClose — every 5 minutes.
|
|
3
|
+
*
|
|
4
|
+
* Flips `prizeRevealStatus` from "open" to "closed" on any prize-draw
|
|
5
|
+
* product whose `prizeRevealWindowEnd` has passed. Pair function to
|
|
6
|
+
* `prizeRevealOpen`. Orders past the deadline are handled by `prizeRevealExpiry`.
|
|
7
|
+
*/
|
|
8
|
+
const PRODUCT_COLLECTION = "products";
|
|
9
|
+
export const prizeRevealCloseHandler = async (ctx) => {
|
|
10
|
+
ctx.logger.info("Prize reveal close sweep starting");
|
|
11
|
+
const snap = await ctx.db
|
|
12
|
+
.collection(PRODUCT_COLLECTION)
|
|
13
|
+
.where("listingType", "==", "prize-draw")
|
|
14
|
+
.where("prizeRevealStatus", "==", "open")
|
|
15
|
+
.where("prizeRevealWindowEnd", "<=", ctx.now)
|
|
16
|
+
.limit(100)
|
|
17
|
+
.get();
|
|
18
|
+
if (snap.empty) {
|
|
19
|
+
ctx.logger.info("No prize draws to close");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const batch = ctx.db.batch();
|
|
23
|
+
snap.docs.forEach((doc) => batch.update(doc.ref, {
|
|
24
|
+
prizeRevealStatus: "closed",
|
|
25
|
+
updatedAt: ctx.now,
|
|
26
|
+
}));
|
|
27
|
+
await batch.commit();
|
|
28
|
+
ctx.logger.info("Closed prize draws", { count: snap.size });
|
|
29
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduledPrizeRevealExpiry — every 6 hours (SB8-B).
|
|
3
|
+
*
|
|
4
|
+
* Finds paid prize-draw orders where `prizeRevealDeadline < now` AND
|
|
5
|
+
* `prizeWon` is unset. Marks them as REFUNDED + notifies the buyer. The
|
|
6
|
+
* actual Razorpay refund is queued — this handler only flips the order
|
|
7
|
+
* status so the buyer's allowance is freed up immediately.
|
|
8
|
+
*/
|
|
9
|
+
import type { ScheduleHandler } from "../runtime/types";
|
|
10
|
+
export declare const prizeRevealExpiryHandler: ScheduleHandler;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduledPrizeRevealExpiry — every 6 hours (SB8-B).
|
|
3
|
+
*
|
|
4
|
+
* Finds paid prize-draw orders where `prizeRevealDeadline < now` AND
|
|
5
|
+
* `prizeWon` is unset. Marks them as REFUNDED + notifies the buyer. The
|
|
6
|
+
* actual Razorpay refund is queued — this handler only flips the order
|
|
7
|
+
* status so the buyer's allowance is freed up immediately.
|
|
8
|
+
*/
|
|
9
|
+
import { notificationRepository } from "../../../../repositories";
|
|
10
|
+
const ORDER_COLLECTION = "orders";
|
|
11
|
+
export const prizeRevealExpiryHandler = async (ctx) => {
|
|
12
|
+
ctx.logger.info("Prize reveal expiry sweep starting");
|
|
13
|
+
const snap = await ctx.db
|
|
14
|
+
.collection(ORDER_COLLECTION)
|
|
15
|
+
.where("paymentStatus", "==", "paid")
|
|
16
|
+
.where("status", "in", ["pending", "confirmed", "processing"])
|
|
17
|
+
.where("prizeRevealDeadline", "<", ctx.now)
|
|
18
|
+
.limit(200)
|
|
19
|
+
.get();
|
|
20
|
+
if (snap.empty) {
|
|
21
|
+
ctx.logger.info("No prize-draw orders past deadline");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let refunded = 0;
|
|
25
|
+
for (const doc of snap.docs) {
|
|
26
|
+
const order = doc.data();
|
|
27
|
+
if (order.prizeWon)
|
|
28
|
+
continue; // already revealed
|
|
29
|
+
if (!order.prizeDrawProductId)
|
|
30
|
+
continue; // not a prize-draw order
|
|
31
|
+
await doc.ref.update({
|
|
32
|
+
status: "refunded",
|
|
33
|
+
paymentStatus: "refunded",
|
|
34
|
+
isNonRefundable: false,
|
|
35
|
+
prizeRevealExpired: true,
|
|
36
|
+
updatedAt: ctx.now,
|
|
37
|
+
});
|
|
38
|
+
if (order.userId) {
|
|
39
|
+
try {
|
|
40
|
+
await notificationRepository.create({
|
|
41
|
+
userId: order.userId,
|
|
42
|
+
type: "prize_reveal_expired",
|
|
43
|
+
priority: "high",
|
|
44
|
+
title: "Reveal deadline passed — refunded",
|
|
45
|
+
message: `Your entry for "${order.productTitle ?? "the draw"}" expired without a reveal. We've refunded the order.`,
|
|
46
|
+
relatedId: doc.id,
|
|
47
|
+
relatedType: "order",
|
|
48
|
+
actionUrl: `/user/orders/view/${doc.id}`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
ctx.logger.warn("Reveal-expired notification failed", { err });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
refunded++;
|
|
56
|
+
}
|
|
57
|
+
ctx.logger.info("Prize reveal expiry sweep complete", { refunded });
|
|
58
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduledPrizeRevealOpen — every 5 minutes.
|
|
3
|
+
*
|
|
4
|
+
* Flips `prizeRevealStatus` from "pending" to "open" on any prize-draw
|
|
5
|
+
* product whose `prizeRevealWindowStart` has passed. Then (SB8-D) emits an
|
|
6
|
+
* in-app notification to every buyer with an unrevealed paid order for that
|
|
7
|
+
* draw: "Your reveal is ready — X reveals waiting. Reveal by [deadline]."
|
|
8
|
+
*/
|
|
9
|
+
import type { ScheduleHandler } from "../runtime/types";
|
|
10
|
+
export declare const prizeRevealOpenHandler: ScheduleHandler;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduledPrizeRevealOpen — every 5 minutes.
|
|
3
|
+
*
|
|
4
|
+
* Flips `prizeRevealStatus` from "pending" to "open" on any prize-draw
|
|
5
|
+
* product whose `prizeRevealWindowStart` has passed. Then (SB8-D) emits an
|
|
6
|
+
* in-app notification to every buyer with an unrevealed paid order for that
|
|
7
|
+
* draw: "Your reveal is ready — X reveals waiting. Reveal by [deadline]."
|
|
8
|
+
*/
|
|
9
|
+
import { notificationRepository } from "../../../../repositories";
|
|
10
|
+
const PRODUCT_COLLECTION = "products";
|
|
11
|
+
const ORDER_COLLECTION = "orders";
|
|
12
|
+
export const prizeRevealOpenHandler = async (ctx) => {
|
|
13
|
+
ctx.logger.info("Prize reveal open sweep starting");
|
|
14
|
+
const snap = await ctx.db
|
|
15
|
+
.collection(PRODUCT_COLLECTION)
|
|
16
|
+
.where("listingType", "==", "prize-draw")
|
|
17
|
+
.where("prizeRevealStatus", "==", "pending")
|
|
18
|
+
.where("prizeRevealWindowStart", "<=", ctx.now)
|
|
19
|
+
.limit(100)
|
|
20
|
+
.get();
|
|
21
|
+
if (snap.empty) {
|
|
22
|
+
ctx.logger.info("No prize draws to open");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const doc of snap.docs) {
|
|
26
|
+
const product = doc.data();
|
|
27
|
+
await doc.ref.update({
|
|
28
|
+
prizeRevealStatus: "open",
|
|
29
|
+
updatedAt: ctx.now,
|
|
30
|
+
});
|
|
31
|
+
// SB8-D — notify buyers with paid unrevealed orders.
|
|
32
|
+
const orders = await ctx.db
|
|
33
|
+
.collection(ORDER_COLLECTION)
|
|
34
|
+
.where("prizeDrawProductId", "==", doc.id)
|
|
35
|
+
.where("paymentStatus", "==", "paid")
|
|
36
|
+
.where("status", "in", ["pending", "confirmed", "processing"])
|
|
37
|
+
.get();
|
|
38
|
+
let notified = 0;
|
|
39
|
+
for (const orderDoc of orders.docs) {
|
|
40
|
+
const order = orderDoc.data();
|
|
41
|
+
if (!order.userId || order.prizeWon)
|
|
42
|
+
continue;
|
|
43
|
+
try {
|
|
44
|
+
await notificationRepository.create({
|
|
45
|
+
userId: order.userId,
|
|
46
|
+
type: "prize_reveal_ready",
|
|
47
|
+
priority: "high",
|
|
48
|
+
title: "Your reveal is ready!",
|
|
49
|
+
message: `Reveal your prize for "${product.title ?? "draw"}". You have ${product.prizeRevealDeadlineDays ?? 3} days to claim it.`,
|
|
50
|
+
relatedId: orderDoc.id,
|
|
51
|
+
relatedType: "order",
|
|
52
|
+
actionUrl: `/user/orders/view/${orderDoc.id}`,
|
|
53
|
+
});
|
|
54
|
+
notified++;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
ctx.logger.warn("Reveal-ready notification failed", { err });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
ctx.logger.info("Opened prize draw", {
|
|
61
|
+
productId: doc.id,
|
|
62
|
+
buyersNotified: notified,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduledPrizeRevealReminder — daily 10am IST (SB8-E).
|
|
3
|
+
*
|
|
4
|
+
* Notifies users whose prize-draw reveals expire in the next 24 hours and
|
|
5
|
+
* haven't been claimed yet. Triggered once a day so users get exactly one
|
|
6
|
+
* nudge before the auto-refund kicks in.
|
|
7
|
+
*/
|
|
8
|
+
import type { ScheduleHandler } from "../runtime/types";
|
|
9
|
+
export declare const prizeRevealReminderHandler: ScheduleHandler;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduledPrizeRevealReminder — daily 10am IST (SB8-E).
|
|
3
|
+
*
|
|
4
|
+
* Notifies users whose prize-draw reveals expire in the next 24 hours and
|
|
5
|
+
* haven't been claimed yet. Triggered once a day so users get exactly one
|
|
6
|
+
* nudge before the auto-refund kicks in.
|
|
7
|
+
*/
|
|
8
|
+
import { notificationRepository } from "../../../../repositories";
|
|
9
|
+
const ORDER_COLLECTION = "orders";
|
|
10
|
+
const ONE_DAY_MS = 86400000;
|
|
11
|
+
export const prizeRevealReminderHandler = async (ctx) => {
|
|
12
|
+
ctx.logger.info("Prize reveal reminder sweep starting");
|
|
13
|
+
const cutoff = new Date(ctx.now.getTime() + ONE_DAY_MS);
|
|
14
|
+
const snap = await ctx.db
|
|
15
|
+
.collection(ORDER_COLLECTION)
|
|
16
|
+
.where("paymentStatus", "==", "paid")
|
|
17
|
+
.where("status", "in", ["pending", "confirmed", "processing"])
|
|
18
|
+
.where("prizeRevealDeadline", "<=", cutoff)
|
|
19
|
+
.where("prizeRevealDeadline", ">", ctx.now)
|
|
20
|
+
.limit(500)
|
|
21
|
+
.get();
|
|
22
|
+
let notified = 0;
|
|
23
|
+
for (const doc of snap.docs) {
|
|
24
|
+
const order = doc.data();
|
|
25
|
+
if (!order.userId || order.prizeWon || !order.prizeDrawProductId)
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
await notificationRepository.create({
|
|
29
|
+
userId: order.userId,
|
|
30
|
+
type: "prize_reveal_reminder",
|
|
31
|
+
priority: "normal",
|
|
32
|
+
title: "Reveal expires within 24h",
|
|
33
|
+
message: `Reveal your prize for "${order.productTitle ?? "the draw"}" today — unclaimed reveals are auto-refunded after the deadline.`,
|
|
34
|
+
relatedId: doc.id,
|
|
35
|
+
relatedType: "order",
|
|
36
|
+
actionUrl: `/user/orders/view/${doc.id}`,
|
|
37
|
+
});
|
|
38
|
+
notified++;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
ctx.logger.warn("Reveal reminder notification failed", { err });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
ctx.logger.info("Prize reveal reminder sweep complete", { notified });
|
|
45
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* triggerEventRaffle — HTTP callable (SB9-D).
|
|
3
|
+
*
|
|
4
|
+
* Selects a raffle winner for an event using `crypto.randomInt()`. Pool logic
|
|
5
|
+
* keys off the event's `raffleType`:
|
|
6
|
+
* - "top_n_scorers" → sort entries by `points DESC` take top N
|
|
7
|
+
* - "top_n_participants" → sort entries by `createdAt ASC` take first N
|
|
8
|
+
* - "open_raffle" → all CONFIRMED entries are eligible
|
|
9
|
+
*
|
|
10
|
+
* On success the function stamps the winner fields onto the event doc and
|
|
11
|
+
* sets the GitHub permalink for proof-of-fairness.
|
|
12
|
+
*/
|
|
13
|
+
import type { CallableHandler } from "../runtime/types";
|
|
14
|
+
interface TriggerEventRaffleInput {
|
|
15
|
+
eventId: string;
|
|
16
|
+
/** Optional override; otherwise read from event.raffleType. */
|
|
17
|
+
raffleType?: "top_n_scorers" | "top_n_participants" | "open_raffle";
|
|
18
|
+
/** Optional N override; otherwise read from event.raffleTopN. */
|
|
19
|
+
topN?: number;
|
|
20
|
+
}
|
|
21
|
+
interface TriggerEventRaffleResult {
|
|
22
|
+
eventId: string;
|
|
23
|
+
raffleWinnerUserId?: string;
|
|
24
|
+
raffleWinnerDisplayName?: string;
|
|
25
|
+
raffleEntryCount: number;
|
|
26
|
+
alreadyTriggered?: boolean;
|
|
27
|
+
reason?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare const triggerEventRaffleHandler: CallableHandler<TriggerEventRaffleInput, TriggerEventRaffleResult>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* triggerEventRaffle — HTTP callable (SB9-D).
|
|
3
|
+
*
|
|
4
|
+
* Selects a raffle winner for an event using `crypto.randomInt()`. Pool logic
|
|
5
|
+
* keys off the event's `raffleType`:
|
|
6
|
+
* - "top_n_scorers" → sort entries by `points DESC` take top N
|
|
7
|
+
* - "top_n_participants" → sort entries by `createdAt ASC` take first N
|
|
8
|
+
* - "open_raffle" → all CONFIRMED entries are eligible
|
|
9
|
+
*
|
|
10
|
+
* On success the function stamps the winner fields onto the event doc and
|
|
11
|
+
* sets the GitHub permalink for proof-of-fairness.
|
|
12
|
+
*/
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
const EVENTS_COLLECTION = "events";
|
|
15
|
+
const EVENT_ENTRIES_COLLECTION = "eventEntries";
|
|
16
|
+
export const triggerEventRaffleHandler = async (input, ctx) => {
|
|
17
|
+
const eventRef = ctx.db.collection(EVENTS_COLLECTION).doc(input.eventId);
|
|
18
|
+
const eventSnap = await eventRef.get();
|
|
19
|
+
if (!eventSnap.exists) {
|
|
20
|
+
return {
|
|
21
|
+
eventId: input.eventId,
|
|
22
|
+
raffleEntryCount: 0,
|
|
23
|
+
reason: "event_not_found",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const event = eventSnap.data();
|
|
27
|
+
if (event.raffleWinnerUserId) {
|
|
28
|
+
return {
|
|
29
|
+
eventId: input.eventId,
|
|
30
|
+
raffleEntryCount: 0,
|
|
31
|
+
alreadyTriggered: true,
|
|
32
|
+
raffleWinnerUserId: event.raffleWinnerUserId,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const raffleType = input.raffleType ?? event.raffleType ?? "open_raffle";
|
|
36
|
+
const topN = input.topN ?? event.raffleTopN ?? 0;
|
|
37
|
+
const entriesSnap = await ctx.db
|
|
38
|
+
.collection(EVENT_ENTRIES_COLLECTION)
|
|
39
|
+
.where("eventId", "==", input.eventId)
|
|
40
|
+
.where("status", "==", "CONFIRMED")
|
|
41
|
+
.get();
|
|
42
|
+
let pool = entriesSnap.docs.map((d) => ({
|
|
43
|
+
id: d.id,
|
|
44
|
+
data: d.data(),
|
|
45
|
+
}));
|
|
46
|
+
if (raffleType === "top_n_scorers" && topN > 0) {
|
|
47
|
+
pool = pool
|
|
48
|
+
.sort((a, b) => (b.data.points ?? 0) - (a.data.points ?? 0))
|
|
49
|
+
.slice(0, topN);
|
|
50
|
+
}
|
|
51
|
+
else if (raffleType === "top_n_participants" && topN > 0) {
|
|
52
|
+
pool = pool
|
|
53
|
+
.sort((a, b) => {
|
|
54
|
+
const aT = a.data.createdAt instanceof Date
|
|
55
|
+
? a.data.createdAt.getTime()
|
|
56
|
+
: (a.data.createdAt?.toDate?.()?.getTime() ?? 0);
|
|
57
|
+
const bT = b.data.createdAt instanceof Date
|
|
58
|
+
? b.data.createdAt.getTime()
|
|
59
|
+
: (b.data.createdAt?.toDate?.()?.getTime() ?? 0);
|
|
60
|
+
return aT - bT;
|
|
61
|
+
})
|
|
62
|
+
.slice(0, topN);
|
|
63
|
+
}
|
|
64
|
+
if (pool.length === 0) {
|
|
65
|
+
return {
|
|
66
|
+
eventId: input.eventId,
|
|
67
|
+
raffleEntryCount: 0,
|
|
68
|
+
reason: "no_eligible_entries",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const winnerIdx = crypto.randomInt(0, pool.length);
|
|
72
|
+
const winner = pool[winnerIdx];
|
|
73
|
+
await eventRef.update({
|
|
74
|
+
raffleWinnerUserId: winner.data.userId,
|
|
75
|
+
raffleWinnerDisplayName: winner.data.userDisplayName ?? "",
|
|
76
|
+
raffleWinnerEntryId: winner.id,
|
|
77
|
+
raffleTriggeredAt: ctx.now,
|
|
78
|
+
raffleEntryCount: entriesSnap.size,
|
|
79
|
+
raffleGithubFunctionUrl: `${ctx.env("PROOF_OF_FAIRNESS_BASE_URL") ?? ""}/raffles/${input.eventId}.json`,
|
|
80
|
+
updatedAt: ctx.now,
|
|
81
|
+
});
|
|
82
|
+
ctx.logger.info("Event raffle triggered", {
|
|
83
|
+
eventId: input.eventId,
|
|
84
|
+
raffleType,
|
|
85
|
+
poolSize: pool.length,
|
|
86
|
+
winnerId: winner.id,
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
eventId: input.eventId,
|
|
90
|
+
raffleWinnerUserId: winner.data.userId,
|
|
91
|
+
raffleWinnerDisplayName: winner.data.userDisplayName,
|
|
92
|
+
raffleEntryCount: entriesSnap.size,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
@@ -28,10 +28,10 @@ export declare const brandInputSchema: z.ZodObject<{
|
|
|
28
28
|
isActive?: boolean | undefined;
|
|
29
29
|
description?: string | undefined;
|
|
30
30
|
website?: string | undefined;
|
|
31
|
+
displayOrder?: number | undefined;
|
|
31
32
|
logoURL?: string | undefined;
|
|
32
33
|
bannerURL?: string | undefined;
|
|
33
34
|
founded?: number | undefined;
|
|
34
|
-
displayOrder?: number | undefined;
|
|
35
35
|
}>;
|
|
36
36
|
export declare const brandUpdateSchema: z.ZodObject<Omit<{
|
|
37
37
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -50,20 +50,20 @@ export declare const brandUpdateSchema: z.ZodObject<Omit<{
|
|
|
50
50
|
isActive?: boolean | undefined;
|
|
51
51
|
description?: string | undefined;
|
|
52
52
|
website?: string | undefined;
|
|
53
|
+
displayOrder?: number | undefined;
|
|
53
54
|
logoURL?: string | undefined;
|
|
54
55
|
bannerURL?: string | undefined;
|
|
55
56
|
founded?: number | undefined;
|
|
56
|
-
displayOrder?: number | undefined;
|
|
57
57
|
}, {
|
|
58
58
|
country?: string | undefined;
|
|
59
59
|
name?: string | undefined;
|
|
60
60
|
isActive?: boolean | undefined;
|
|
61
61
|
description?: string | undefined;
|
|
62
62
|
website?: string | undefined;
|
|
63
|
+
displayOrder?: number | undefined;
|
|
63
64
|
logoURL?: string | undefined;
|
|
64
65
|
bannerURL?: string | undefined;
|
|
65
66
|
founded?: number | undefined;
|
|
66
|
-
displayOrder?: number | undefined;
|
|
67
67
|
}>;
|
|
68
68
|
export type BrandInput = z.infer<typeof brandInputSchema>;
|
|
69
69
|
export type BrandUpdate = z.infer<typeof brandUpdateSchema>;
|