@mohasinac/appkit 4.2.0 → 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.js +7 -78
- 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 +2 -5
- package/dist/_internal/server/functions/scheduled.js +6 -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/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 +2 -2
- 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/handlers/index.d.ts +3 -4
- package/dist/_internal/server/jobs/handlers/index.js +8 -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/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/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/account/hooks/useNotifications.js +5 -0
- package/dist/features/admin/actions/admin-actions.js +6 -0
- package/dist/features/admin/actions/notification-actions.js +1 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
- package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +33 -6
- 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/schemas/firestore.d.ts +13 -5
- package/dist/features/admin/schemas/firestore.js +1 -2
- 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/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/components/OrdersList.js +1 -5
- package/dist/features/orders/schemas/firestore.d.ts +2 -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 +0 -1
- package/dist/features/products/constants/action-defs.js +0 -2
- 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/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/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 +19 -22
- package/dist/index.js +25 -58
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +2 -2
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.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 +5 -2
- package/dist/server-entry.d.ts +0 -1
- package/dist/server-entry.js +0 -1
- package/dist/server.d.ts +4 -20
- package/dist/server.js +10 -57
- 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
|
@@ -17,11 +17,29 @@ export interface BundleDataOptions {
|
|
|
17
17
|
*/
|
|
18
18
|
export declare const getBundleForDetail: (slug: string, _opts?: BundleDataOptions) => Promise<CategoryDocument | null>;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Resolve the effective member product ids for a bundle. `bundleProductIds`
|
|
21
|
+
* is the index-friendly mirror and is normally authoritative, but a bundle
|
|
22
|
+
* can be written (e.g. by seed data or a partial API payload) with only its
|
|
23
|
+
* `bundleQueryRule` set and no mirror — falls back to the static rule's
|
|
24
|
+
* `productIds` in that case rather than silently rendering an empty bundle.
|
|
23
25
|
*/
|
|
24
|
-
export declare
|
|
26
|
+
export declare function resolveBundleMemberIds(bundle: Pick<CategoryDocument, "bundleProductIds" | "bundleQueryRule">): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Fetch the resolved product members of a bundle. Hydrates each id from
|
|
29
|
+
* `resolveBundleMemberIds` via `productRepository.findById`. Returns an
|
|
30
|
+
* empty array when the bundle has no resolvable members yet.
|
|
31
|
+
*/
|
|
32
|
+
export declare const listBundleMembers: (bundle: Pick<CategoryDocument, "bundleProductIds" | "bundleQueryRule">, _opts?: BundleDataOptions) => Promise<ProductDocument[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Sum the individual prices of a set of member products — the "buy
|
|
35
|
+
* separately" total a bundle's discount is measured against. Used at bundle
|
|
36
|
+
* create/update time (and by the daily `runBundleStockSync` sweep) to
|
|
37
|
+
* populate `bundleOriginalTotal`. Deliberately returns `undefined` (rather
|
|
38
|
+
* than a partial sum) when any member id fails to resolve, so a stale or
|
|
39
|
+
* broken reference never understates the "before" price and inflates the
|
|
40
|
+
* displayed discount.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveBundleOriginalTotal(productIds: string[]): Promise<number | undefined>;
|
|
25
43
|
/**
|
|
26
44
|
* List the most recent active bundles for homepage placement. Bounded to
|
|
27
45
|
* `limit` (default 8) and filtered to `isActive: true` server-side.
|
|
@@ -19,18 +19,55 @@ export const getBundleForDetail = cache(async (slug, _opts) => {
|
|
|
19
19
|
return categoriesRepository.findBySlugAndType(slug, "bundle");
|
|
20
20
|
});
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Resolve the effective member product ids for a bundle. `bundleProductIds`
|
|
23
|
+
* is the index-friendly mirror and is normally authoritative, but a bundle
|
|
24
|
+
* can be written (e.g. by seed data or a partial API payload) with only its
|
|
25
|
+
* `bundleQueryRule` set and no mirror — falls back to the static rule's
|
|
26
|
+
* `productIds` in that case rather than silently rendering an empty bundle.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveBundleMemberIds(bundle) {
|
|
29
|
+
if (bundle.bundleProductIds?.length)
|
|
30
|
+
return bundle.bundleProductIds;
|
|
31
|
+
if (bundle.bundleQueryRule?.type === "static") {
|
|
32
|
+
return bundle.bundleQueryRule.productIds;
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Fetch the resolved product members of a bundle. Hydrates each id from
|
|
38
|
+
* `resolveBundleMemberIds` via `productRepository.findById`. Returns an
|
|
39
|
+
* empty array when the bundle has no resolvable members yet.
|
|
25
40
|
*/
|
|
26
41
|
export const listBundleMembers = cache(async (bundle, _opts) => {
|
|
27
42
|
void _opts;
|
|
28
|
-
const ids = bundle
|
|
43
|
+
const ids = resolveBundleMemberIds(bundle);
|
|
29
44
|
if (ids.length === 0)
|
|
30
45
|
return [];
|
|
31
46
|
const results = await Promise.all(ids.map((id) => productRepository.findById(id).catch(() => null)));
|
|
32
47
|
return results.filter((p) => p !== null);
|
|
33
48
|
});
|
|
49
|
+
/**
|
|
50
|
+
* Sum the individual prices of a set of member products — the "buy
|
|
51
|
+
* separately" total a bundle's discount is measured against. Used at bundle
|
|
52
|
+
* create/update time (and by the daily `runBundleStockSync` sweep) to
|
|
53
|
+
* populate `bundleOriginalTotal`. Deliberately returns `undefined` (rather
|
|
54
|
+
* than a partial sum) when any member id fails to resolve, so a stale or
|
|
55
|
+
* broken reference never understates the "before" price and inflates the
|
|
56
|
+
* displayed discount.
|
|
57
|
+
*/
|
|
58
|
+
export async function resolveBundleOriginalTotal(productIds) {
|
|
59
|
+
if (productIds.length === 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
const results = await Promise.all(productIds.map((id) => productRepository.findById(id).catch(() => null)));
|
|
62
|
+
const prices = [];
|
|
63
|
+
for (const p of results) {
|
|
64
|
+
if (!p || typeof p.price !== "number")
|
|
65
|
+
return undefined;
|
|
66
|
+
prices.push(p.price);
|
|
67
|
+
}
|
|
68
|
+
const total = prices.reduce((sum, p) => sum + p, 0);
|
|
69
|
+
return Math.round(total * 100) / 100;
|
|
70
|
+
}
|
|
34
71
|
/**
|
|
35
72
|
* List the most recent active bundles for homepage placement. Bounded to
|
|
36
73
|
* `limit` (default 8) and filtered to `isActive: true` server-side.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, type BundleDataOptions, } from "./data";
|
|
1
|
+
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, type BundleDataOptions, } from "./data";
|
|
2
2
|
export { buildBundleMetadata, type BundleMetadataOptions } from "./metadata";
|
|
3
3
|
export { renderBundleOg, renderBundleOgImage, type BundleOgData, } from "./og";
|
|
4
4
|
export { addBundleToCartAction } from "./actions";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, } from "./data";
|
|
1
|
+
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, } from "./data";
|
|
2
2
|
export { buildBundleMetadata } from "./metadata";
|
|
3
3
|
// S-SBUNI-4 2026-05-13 — bundle OG renderer.
|
|
4
4
|
export { renderBundleOg, renderBundleOgImage, } from "./og";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { normalizeError } from "../../../../errors/normalize";
|
|
2
|
-
import { safeFireAndForget } from "../../../../utils/safe-fire-forget";
|
|
3
2
|
import { roundRupees } from "../../../../utils/number.formatter";
|
|
4
3
|
/**
|
|
5
4
|
* Checkout server actions (appkit).
|
|
@@ -18,21 +17,19 @@ import { computePreOrderDepositAmount } from "../../../shared/checkout/order-mat
|
|
|
18
17
|
import { failedCheckoutRepository } from "../../../../features/checkout/repository/failed-checkout.repository";
|
|
19
18
|
import { sendOrderConfirmationEmail } from "../../../../features/contact/server";
|
|
20
19
|
import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
|
|
21
|
-
import { resolveDate } from "../../../../utils";
|
|
22
20
|
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, } from "../../../shared/fees/calculator";
|
|
23
21
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase";
|
|
24
22
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore";
|
|
25
23
|
import { CART_COLLECTION } from "../../../../features/cart/schemas/index";
|
|
26
24
|
// S-SBUNI-RULES 2026-05-13 — bundle cart-line stock fan-out helpers.
|
|
27
25
|
import { getCartItemMemberIds, getExpandedDecrements, bucketCartItemsByStock, } from "./bundle-expansion";
|
|
28
|
-
import { consentOtpRef, consentOtpRateLimitRef, CONSENT_OTP_MAX_BYPASS_CREDITS, } from "../../../../features/auth/server";
|
|
29
26
|
import { OrderStatusValues, PaymentStatusValues, PaymentMethodValues, OutOfStockPolicyValues, } from "../../../../features/orders/schemas/index";
|
|
30
27
|
import { getDefaultCurrency } from "../../../../core/index";
|
|
31
28
|
import { verifyPaymentSignatureWithKeys, fetchRazorpayOrder, paiseToRupees, } from "../../../../providers/payment-razorpay/index";
|
|
32
29
|
import { CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_DEFAULT_EMI_SETTINGS } from "../../../shared/features/checkout/config";
|
|
33
30
|
import { checkEmiEligibility, computeEmiSchedule } from "../../../shared/features/emi/schedule";
|
|
34
31
|
import { formatShippingAddress } from "./data";
|
|
35
|
-
import { enforceMaxPerUserForCart
|
|
32
|
+
import { enforceMaxPerUserForCart } from "./prize-bundle-gates";
|
|
36
33
|
import { getListingRule, runSyncPreflight, } from "../../../shared/checkout/rules";
|
|
37
34
|
import { cartIsDigitalOnly } from "../../../shared/listing-types/cart-shipping";
|
|
38
35
|
import { processRefundAction } from "../refunds/actions";
|
|
@@ -215,18 +212,6 @@ async function flushCouponUsageAccumulator(accumulator, uid) {
|
|
|
215
212
|
serverLogger.error("Failed to record coupon usage:", { err: err instanceof Error ? err.message : String(err) });
|
|
216
213
|
}
|
|
217
214
|
}
|
|
218
|
-
function grantConsentOtpBypassCredit(db, uid, emailOtpUsed, unavailableCount) {
|
|
219
|
-
if (!emailOtpUsed || unavailableCount === 0)
|
|
220
|
-
return;
|
|
221
|
-
const metaRef = consentOtpRateLimitRef(db, uid);
|
|
222
|
-
metaRef
|
|
223
|
-
.get()
|
|
224
|
-
.then((snap) => {
|
|
225
|
-
const cur = snap.exists ? (snap.data()?.bypassCredits ?? 0) : 0;
|
|
226
|
-
return metaRef.set({ bypassCredits: Math.min(cur + 1, CONSENT_OTP_MAX_BYPASS_CREDITS) }, { merge: true });
|
|
227
|
-
})
|
|
228
|
-
.catch((err) => serverLogger.warn("Failed to grant consent OTP bypass credit", { err }));
|
|
229
|
-
}
|
|
230
215
|
function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
231
216
|
if (emailsToSend.length === 0)
|
|
232
217
|
return;
|
|
@@ -382,9 +367,6 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
382
367
|
const groupRule = getListingRule(lt0);
|
|
383
368
|
const extraOrderFields = {
|
|
384
369
|
...groupRule.decorateOrderDoc(group[0].item, group[0].product),
|
|
385
|
-
...(orderType === "prize-draw"
|
|
386
|
-
? { prizeRevealDeadline: computePrizeRevealDeadline(group[0].product) }
|
|
387
|
-
: {}),
|
|
388
370
|
};
|
|
389
371
|
// ── 15-minute payment window + seller UPI resolution (Tier PP) ──────────
|
|
390
372
|
// Deadline + displayed UPI apply only to methods with a buyer-uploaded
|
|
@@ -577,7 +559,6 @@ export async function createCheckoutOrderAction(input) {
|
|
|
577
559
|
shippingAddress = formatShippingAddress(resolvedAddress);
|
|
578
560
|
}
|
|
579
561
|
const db = getAdminDb();
|
|
580
|
-
const otpRef = isDigitalCart ? null : consentOtpRef(db, uid, addressId);
|
|
581
562
|
// SB6-C — pre-tx fetch products so we can run the maxPerUser cap check
|
|
582
563
|
// (count queries can't run inside a Firestore transaction). The same
|
|
583
564
|
// product docs are re-read inside the transaction below for stock + prize
|
|
@@ -613,21 +594,6 @@ export async function createCheckoutOrderAction(input) {
|
|
|
613
594
|
let stockResult;
|
|
614
595
|
try {
|
|
615
596
|
stockResult = await db.runTransaction(async (tx) => {
|
|
616
|
-
let emailOtpUsed = false;
|
|
617
|
-
if (!isDigitalCart && otpRef) {
|
|
618
|
-
const otpSnap = await tx.get(otpRef);
|
|
619
|
-
const otpData = otpSnap.exists
|
|
620
|
-
? otpSnap.data()
|
|
621
|
-
: null;
|
|
622
|
-
const isConsentValid = otpData?.verified === true &&
|
|
623
|
-
otpData.expiresAt &&
|
|
624
|
-
(resolveDate(otpData.expiresAt)?.getTime() ?? 0) > Date.now();
|
|
625
|
-
if (!isConsentValid) {
|
|
626
|
-
const reason = !otpData ? "otp_not_verified" : "consent_expired";
|
|
627
|
-
throw Object.assign(new ApiError(403, "Order verification required. Please complete OTP verification before placing this order."), { _failReason: reason });
|
|
628
|
-
}
|
|
629
|
-
emailOtpUsed = otpData.verifiedVia !== "sms" && otpData.verifiedVia !== "admin_bypass";
|
|
630
|
-
}
|
|
631
597
|
// SB-UNI-5 2026-05-13 — bundle-aware stock fan-out. Build the unique
|
|
632
598
|
// product-id set across the whole cart (regular items + each bundle's
|
|
633
599
|
// members) and fetch each one ONCE. Validation walks per cart-line and
|
|
@@ -662,7 +628,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
662
628
|
// S-SBUNI-RULES 2026-05-13 — sync preflight (prize-pool cap, pre-order
|
|
663
629
|
// quota) via rule registry. Bundles skip — they bypass the cart path.
|
|
664
630
|
if (!item.bundleProductIds?.length) {
|
|
665
|
-
runSyncPreflight([{ item, product }]);
|
|
631
|
+
runSyncPreflight([{ item, product }], paymentMethod);
|
|
666
632
|
}
|
|
667
633
|
availableItems.push({ item, product });
|
|
668
634
|
}
|
|
@@ -687,29 +653,21 @@ export async function createCheckoutOrderAction(input) {
|
|
|
687
653
|
selectedItemIds: null,
|
|
688
654
|
updatedAt: new Date(),
|
|
689
655
|
}, { merge: true });
|
|
690
|
-
if (otpRef) {
|
|
691
|
-
tx.delete(otpRef);
|
|
692
|
-
}
|
|
693
656
|
}
|
|
694
|
-
return { available: availableItems, unavailable: unavailableItems
|
|
657
|
+
return { available: availableItems, unavailable: unavailableItems };
|
|
695
658
|
});
|
|
696
659
|
}
|
|
697
660
|
catch (err) {
|
|
698
661
|
void normalizeError(err);
|
|
699
|
-
const reason = err?._failReason === "consent_expired"
|
|
700
|
-
? "consent_expired"
|
|
701
|
-
: err?._failReason === "otp_not_verified"
|
|
702
|
-
? "otp_not_verified"
|
|
703
|
-
: "unknown";
|
|
704
662
|
failedCheckoutRepository
|
|
705
|
-
.logCheckout(uid,
|
|
663
|
+
.logCheckout(uid, "unknown", err instanceof Error ? err.message : String(err), {
|
|
706
664
|
addressId,
|
|
707
665
|
paymentMethod,
|
|
708
666
|
})
|
|
709
667
|
.catch((logErr) => { void normalizeError(logErr); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: logErr instanceof Error ? logErr.message : String(logErr) }); });
|
|
710
668
|
throw err;
|
|
711
669
|
}
|
|
712
|
-
const { available, unavailable
|
|
670
|
+
const { available, unavailable } = stockResult;
|
|
713
671
|
if (available.length === 0) {
|
|
714
672
|
failedCheckoutRepository
|
|
715
673
|
.logCheckout(uid, "stock_failed", "All items out of stock", {
|
|
@@ -764,7 +722,6 @@ export async function createCheckoutOrderAction(input) {
|
|
|
764
722
|
total += await createOrderForGroup(group, orderType, groupCtx);
|
|
765
723
|
}
|
|
766
724
|
await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
|
|
767
|
-
grantConsentOtpBypassCredit(db, uid, emailOtpUsed, unavailable.length);
|
|
768
725
|
dispatchOrderConfirmationEmails(emailsToSend);
|
|
769
726
|
serverLogger.info(`createCheckoutOrderAction: ${orderIds.length} order(s) placed for uid=${uid}` +
|
|
770
727
|
(unavailable.length > 0 ? ` (${unavailable.length} item(s) unavailable)` : ""));
|
|
@@ -993,9 +950,6 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
993
950
|
const groupRuleRzp = getListingRule(lt0Rzp);
|
|
994
951
|
const extraOrderFields = {
|
|
995
952
|
...groupRuleRzp.decorateOrderDoc(group[0].item, group[0].product),
|
|
996
|
-
...(orderType === "prize-draw" && group[0].product
|
|
997
|
-
? { prizeRevealDeadline: computePrizeRevealDeadline(group[0].product) }
|
|
998
|
-
: {}),
|
|
999
953
|
};
|
|
1000
954
|
const order = await unitOfWork.orders.create({
|
|
1001
955
|
productId: firstItem.productId,
|
|
@@ -1127,28 +1081,6 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1127
1081
|
}
|
|
1128
1082
|
shippingAddress = formatShippingAddress(resolvedAddressRzp);
|
|
1129
1083
|
}
|
|
1130
|
-
const db = getAdminDb();
|
|
1131
|
-
if (!isDigitalCartRazorpay && addressId) {
|
|
1132
|
-
const otpRef = consentOtpRef(db, uid, addressId);
|
|
1133
|
-
const otpSnap = await otpRef.get();
|
|
1134
|
-
const otpData = otpSnap.exists
|
|
1135
|
-
? otpSnap.data()
|
|
1136
|
-
: null;
|
|
1137
|
-
const isConsentValid = otpData?.verified === true &&
|
|
1138
|
-
otpData.expiresAt &&
|
|
1139
|
-
(resolveDate(otpData.expiresAt)?.getTime() ?? 0) > Date.now();
|
|
1140
|
-
if (!isConsentValid) {
|
|
1141
|
-
const reason = !otpData ? "otp_not_verified" : "consent_expired";
|
|
1142
|
-
failedCheckoutRepository
|
|
1143
|
-
.logPayment(uid, reason, "Consent OTP missing or expired at payment verify time", {
|
|
1144
|
-
gatewayOrderId: razorpay_order_id,
|
|
1145
|
-
gatewayPaymentId: razorpay_payment_id,
|
|
1146
|
-
addressId,
|
|
1147
|
-
})
|
|
1148
|
-
.catch((err) => { void normalizeError(err); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: err instanceof Error ? err.message : String(err) }); });
|
|
1149
|
-
throw new ApiError(403, "Order verification required. Please complete OTP verification and retry.");
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
1084
|
// SB-UNI-5 2026-05-13 — bundle-aware product fetch + validation. Each
|
|
1153
1085
|
// cart line gets paired with a "representative" product (first member id
|
|
1154
1086
|
// for bundles, productId for regular items). The full bundle-member
|
|
@@ -1179,7 +1111,8 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1179
1111
|
// S-SBUNI-RULES 2026-05-13 — sync preflight via rule registry.
|
|
1180
1112
|
// Bundle items bypass the cart flow so skip the prize-pool cap.
|
|
1181
1113
|
const preflightPairs = productChecks.filter((p) => p.product !== null && p.product !== undefined && !p.item.bundleProductIds?.length);
|
|
1182
|
-
|
|
1114
|
+
// This whole action is the Razorpay path — paymentMethod is always "online".
|
|
1115
|
+
runSyncPreflight(preflightPairs, "online");
|
|
1183
1116
|
// SB-UNI-O 2026-05-15 — Live-item jurisdiction guard.
|
|
1184
1117
|
if (!isDigitalCartRazorpay && resolvedAddressRzp) {
|
|
1185
1118
|
assertLiveJurisdiction(cart.items, productByIdPaid, resolvedAddressRzp.state);
|
|
@@ -1310,10 +1243,6 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1310
1243
|
selectedItemIds: null,
|
|
1311
1244
|
});
|
|
1312
1245
|
});
|
|
1313
|
-
if (!isDigitalCartRazorpay && addressId) {
|
|
1314
|
-
const otpRefForDelete = consentOtpRef(db, uid, addressId);
|
|
1315
|
-
safeFireAndForget(otpRefForDelete.delete(), "checkout: delete OTP consent ref after Razorpay payment");
|
|
1316
|
-
}
|
|
1317
1246
|
// outOfStockPolicy: "skip_items" — auto-refund the dropped items' value.
|
|
1318
1247
|
// Razorpay already captured payment for the FULL cart before this stock
|
|
1319
1248
|
// check ran, so anything not placed as an order must be given back.
|
|
@@ -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,
|
|
@@ -19,11 +19,8 @@ export declare const cleanupRtdbEvents: import("./types").ScheduledFunctionDefin
|
|
|
19
19
|
export declare const mediaTmpCleanup: import("./types").ScheduledFunctionDefinition;
|
|
20
20
|
export declare const draftPrune: import("./types").ScheduledFunctionDefinition;
|
|
21
21
|
export declare const testerSandboxCleanup: import("./types").ScheduledFunctionDefinition;
|
|
22
|
-
export declare const
|
|
23
|
-
export declare const prizeRevealClose: import("./types").ScheduledFunctionDefinition;
|
|
24
|
-
export declare const prizeRevealExpiry: import("./types").ScheduledFunctionDefinition;
|
|
25
|
-
export declare const prizeRevealReminder: import("./types").ScheduledFunctionDefinition;
|
|
22
|
+
export declare const prizeDrawExpiryReveal: import("./types").ScheduledFunctionDefinition;
|
|
26
23
|
export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
|
|
27
24
|
export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
|
|
28
25
|
export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
|
|
29
|
-
export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition
|
|
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];
|