@mohasinac/appkit 2.6.0 → 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/api/routeHandler.js +6 -4
- 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/ui/components/Button.js +21 -2
- package/dist/ui/components/Button.style.css +34 -0
- package/dist/validation/schemas.d.ts +8 -8
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +2 -4
|
@@ -8,19 +8,64 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ApiError, ValidationError, NotFoundError, ERROR_MESSAGES } from "../../../../errors";
|
|
10
10
|
import { serverLogger } from "../../../../monitoring";
|
|
11
|
-
import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository, } from "../../../../repositories";
|
|
11
|
+
import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository, notificationRepository, } from "../../../../repositories";
|
|
12
12
|
import { failedCheckoutRepository } from "../../../../features/checkout/repository/failed-checkout.repository";
|
|
13
13
|
import { sendOrderConfirmationEmail } from "../../../../features/contact/server";
|
|
14
14
|
import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
|
|
15
15
|
import { resolveDate } from "../../../../utils";
|
|
16
|
-
import { getAdminDb } from "../../../../providers/db-firebase";
|
|
16
|
+
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase";
|
|
17
17
|
import { PRODUCT_COLLECTION, ProductStatusValues } from "../../../../features/products/schemas/firestore";
|
|
18
18
|
import { CART_COLLECTION } from "../../../../features/cart/schemas/index";
|
|
19
19
|
import { consentOtpRef, consentOtpRateLimitRef, CONSENT_OTP_MAX_BYPASS_CREDITS, } from "../../../../features/auth/server";
|
|
20
|
-
import { OrderStatusValues, PaymentStatusValues } from "../../../../features/orders/schemas/index";
|
|
20
|
+
import { OrderStatusValues, PaymentStatusValues, PaymentMethodValues, } from "../../../../features/orders/schemas/index";
|
|
21
21
|
import { getDefaultCurrency } from "../../../../core/index";
|
|
22
|
+
import { verifyPaymentSignatureWithKeys, fetchRazorpayOrder, paiseToRupees, } from "../../../../providers/payment-razorpay/index";
|
|
22
23
|
import { CHECKOUT_DEFAULT_COMMISSIONS } from "../../../shared/features/checkout/config";
|
|
23
24
|
import { formatShippingAddress } from "./data";
|
|
25
|
+
import { enforceMaxPerUserForCart, enforcePrizePoolCap, computePrizeRevealDeadline, } from "./prize-bundle-gates";
|
|
26
|
+
/**
|
|
27
|
+
* Fire-and-forget in-app notifications when an order is created.
|
|
28
|
+
*
|
|
29
|
+
* The Cloud Function `onOrderStatusChange` only fires on status transitions
|
|
30
|
+
* (not on creates), so a brand-new order never produces an in-app row for
|
|
31
|
+
* either party. We emit the buyer + seller rows here at the create boundary.
|
|
32
|
+
*/
|
|
33
|
+
function emitOrderPlacedNotifications(args) {
|
|
34
|
+
const { orderId, buyerUid, buyerName, storeOwnerId, productLabel, paid } = args;
|
|
35
|
+
const buyerNotif = notificationRepository
|
|
36
|
+
.create({
|
|
37
|
+
userId: buyerUid,
|
|
38
|
+
type: "order_placed",
|
|
39
|
+
priority: "normal",
|
|
40
|
+
title: "Order placed",
|
|
41
|
+
message: `Your order for ${productLabel} has been placed.`,
|
|
42
|
+
relatedId: orderId,
|
|
43
|
+
relatedType: "order",
|
|
44
|
+
actionUrl: `/user/orders/view/${orderId}`,
|
|
45
|
+
})
|
|
46
|
+
.catch((err) => serverLogger.warn("Failed to create buyer order_placed notification", {
|
|
47
|
+
err,
|
|
48
|
+
orderId,
|
|
49
|
+
}));
|
|
50
|
+
const sellerNotif = storeOwnerId
|
|
51
|
+
? notificationRepository
|
|
52
|
+
.create({
|
|
53
|
+
userId: storeOwnerId,
|
|
54
|
+
type: "order_placed",
|
|
55
|
+
priority: "high",
|
|
56
|
+
title: "New order received",
|
|
57
|
+
message: `${paid ? "New paid order" : "New order"} from ${buyerName || "a buyer"} for ${productLabel}.`,
|
|
58
|
+
relatedId: orderId,
|
|
59
|
+
relatedType: "order",
|
|
60
|
+
actionUrl: `/store/orders/${orderId}/view`,
|
|
61
|
+
})
|
|
62
|
+
.catch((err) => serverLogger.warn("Failed to create seller order_placed notification", {
|
|
63
|
+
err,
|
|
64
|
+
orderId,
|
|
65
|
+
}))
|
|
66
|
+
: Promise.resolve();
|
|
67
|
+
void Promise.all([buyerNotif, sellerNotif]);
|
|
68
|
+
}
|
|
24
69
|
/**
|
|
25
70
|
* Place order(s) from the user's cart in a single Firestore transaction.
|
|
26
71
|
*
|
|
@@ -54,6 +99,22 @@ export async function createCheckoutOrderAction(input) {
|
|
|
54
99
|
const shippingAddress = formatShippingAddress(address);
|
|
55
100
|
const db = getAdminDb();
|
|
56
101
|
const otpRef = consentOtpRef(db, uid, addressId);
|
|
102
|
+
// SB6-C — pre-tx fetch products so we can run the maxPerUser cap check
|
|
103
|
+
// (count queries can't run inside a Firestore transaction). The same
|
|
104
|
+
// product docs are re-read inside the transaction below for stock + prize
|
|
105
|
+
// pool checks; this is intentional (the tx is the source of truth for the
|
|
106
|
+
// counts we actually mutate).
|
|
107
|
+
const preTxProductRefs = cartItems.map((item) => db.collection(PRODUCT_COLLECTION).doc(item.productId));
|
|
108
|
+
const preTxProductSnaps = await Promise.all(preTxProductRefs.map((r) => r.get()));
|
|
109
|
+
const preTxPairs = cartItems
|
|
110
|
+
.map((item, i) => {
|
|
111
|
+
const data = preTxProductSnaps[i].exists
|
|
112
|
+
? preTxProductSnaps[i].data()
|
|
113
|
+
: undefined;
|
|
114
|
+
return data ? { item, product: data } : null;
|
|
115
|
+
})
|
|
116
|
+
.filter((pair) => pair !== null);
|
|
117
|
+
await enforceMaxPerUserForCart({ userId: uid, items: preTxPairs });
|
|
57
118
|
let stockResult;
|
|
58
119
|
try {
|
|
59
120
|
stockResult = await db.runTransaction(async (tx) => {
|
|
@@ -88,10 +149,20 @@ export async function createCheckoutOrderAction(input) {
|
|
|
88
149
|
});
|
|
89
150
|
}
|
|
90
151
|
else {
|
|
91
|
-
tx
|
|
152
|
+
// SB6-C — prize-draw pool cap (in-tx, atomic with availableQuantity).
|
|
153
|
+
enforcePrizePoolCap({
|
|
154
|
+
productSnapshot: productData,
|
|
155
|
+
requestedQuantity: item.quantity,
|
|
156
|
+
});
|
|
157
|
+
const productUpdate = {
|
|
92
158
|
availableQuantity: productData.availableQuantity - item.quantity,
|
|
93
159
|
updatedAt: new Date(),
|
|
94
|
-
}
|
|
160
|
+
};
|
|
161
|
+
if (productData.listingType === "prize-draw") {
|
|
162
|
+
productUpdate.prizeCurrentEntries =
|
|
163
|
+
(productData.prizeCurrentEntries ?? 0) + item.quantity;
|
|
164
|
+
}
|
|
165
|
+
tx.update(productRefs[i], productUpdate);
|
|
95
166
|
availableItems.push({ item, product: productData });
|
|
96
167
|
}
|
|
97
168
|
}
|
|
@@ -146,19 +217,38 @@ export async function createCheckoutOrderAction(input) {
|
|
|
146
217
|
const firstItem = group[0].item;
|
|
147
218
|
const groupTotal = group.reduce((sum, { item }) => sum + item.price * item.quantity, 0);
|
|
148
219
|
total += groupTotal;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
})
|
|
220
|
+
// SB8-F — when a line is a prize-draw entry, stamp the listingType +
|
|
221
|
+
// reveal status + deadline so the user-orders surface can render the
|
|
222
|
+
// reveals-remaining badge immediately after order creation.
|
|
223
|
+
const groupRevealDeadline = orderType === "prize-draw"
|
|
224
|
+
? computePrizeRevealDeadline(group[0].product)
|
|
225
|
+
: undefined;
|
|
226
|
+
const orderItems = group.map(({ item, product }) => {
|
|
227
|
+
const isPrizeDrawLine = product.listingType === "prize-draw";
|
|
228
|
+
return {
|
|
229
|
+
productId: item.productId,
|
|
230
|
+
productTitle: item.productTitle,
|
|
231
|
+
quantity: item.quantity,
|
|
232
|
+
unitPrice: item.price,
|
|
233
|
+
totalPrice: item.price * item.quantity,
|
|
234
|
+
...(isPrizeDrawLine
|
|
235
|
+
? {
|
|
236
|
+
listingType: "prize-draw",
|
|
237
|
+
prizeRevealStatus: product.prizeRevealStatus === "open" ? "open" : "pending",
|
|
238
|
+
prizeRevealDeadline: groupRevealDeadline?.toISOString(),
|
|
239
|
+
}
|
|
240
|
+
: product.listingType
|
|
241
|
+
? { listingType: product.listingType }
|
|
242
|
+
: {}),
|
|
243
|
+
};
|
|
244
|
+
});
|
|
156
245
|
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
157
246
|
let shippingFee = 0;
|
|
247
|
+
let storeOwnerId;
|
|
158
248
|
const storeId = firstItem.storeId;
|
|
159
249
|
if (storeId) {
|
|
160
250
|
const store = await storeRepository.findById(storeId);
|
|
161
|
-
|
|
251
|
+
storeOwnerId = store?.ownerId;
|
|
162
252
|
if (storeOwnerId) {
|
|
163
253
|
const sellerUser = await userRepository.findById(storeOwnerId);
|
|
164
254
|
const shippingConfig = sellerUser?.shippingConfig;
|
|
@@ -237,6 +327,17 @@ export async function createCheckoutOrderAction(input) {
|
|
|
237
327
|
.map(({ product }) => product.mainImage)
|
|
238
328
|
.filter((url) => typeof url === "string" && url.length > 0)),
|
|
239
329
|
];
|
|
330
|
+
// SB-UNI-D — "bundle" order-type removed; bundle cart lines will expand
|
|
331
|
+
// to N product order lines at checkout (forward-looking feature, not
|
|
332
|
+
// wired yet). Prize-draw fields only.
|
|
333
|
+
const isPrizeDrawOrder = orderType === "prize-draw";
|
|
334
|
+
const prizeDrawFields = isPrizeDrawOrder
|
|
335
|
+
? {
|
|
336
|
+
prizeDrawProductId: group[0].product.id,
|
|
337
|
+
isNonRefundable: true,
|
|
338
|
+
prizeRevealDeadline: computePrizeRevealDeadline(group[0].product),
|
|
339
|
+
}
|
|
340
|
+
: {};
|
|
240
341
|
const order = await unitOfWork.orders.create({
|
|
241
342
|
productId: firstItem.productId,
|
|
242
343
|
productTitle: firstItem.productTitle,
|
|
@@ -264,6 +365,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
264
365
|
couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
|
|
265
366
|
appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
|
|
266
367
|
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
|
368
|
+
...prizeDrawFields,
|
|
267
369
|
});
|
|
268
370
|
orderIds.push(order.id);
|
|
269
371
|
for (const code of groupCouponCodes) {
|
|
@@ -285,6 +387,14 @@ export async function createCheckoutOrderAction(input) {
|
|
|
285
387
|
items: orderItems,
|
|
286
388
|
});
|
|
287
389
|
}
|
|
390
|
+
emitOrderPlacedNotifications({
|
|
391
|
+
orderId: order.id,
|
|
392
|
+
buyerUid: uid,
|
|
393
|
+
buyerName: userName,
|
|
394
|
+
storeOwnerId,
|
|
395
|
+
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
396
|
+
paid: false,
|
|
397
|
+
});
|
|
288
398
|
}
|
|
289
399
|
if (couponUsageAccumulator.size > 0) {
|
|
290
400
|
Promise.all([...couponUsageAccumulator.values()].map(({ couponId, code, orderIds: ids, totalDiscount }) => couponsRepository.applyCoupon(couponId, code, uid, ids, totalDiscount))).catch((err) => serverLogger.error("Failed to record coupon usage:", err));
|
|
@@ -325,3 +435,322 @@ export async function attachPaymentAction(input) {
|
|
|
325
435
|
updatedAt: new Date(),
|
|
326
436
|
});
|
|
327
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Place order(s) from the user's cart after a Razorpay payment is verified.
|
|
440
|
+
* Mirrors the existing /api/payment/verify route handler.
|
|
441
|
+
*
|
|
442
|
+
* Consumers must authenticate the user before calling. The action performs:
|
|
443
|
+
* 1. HMAC signature verification (rejects forged callbacks)
|
|
444
|
+
* 2. Cart re-validation against current product prices/stock
|
|
445
|
+
* 3. Amount cross-check against the Razorpay order record
|
|
446
|
+
* 4. Atomic stock decrement + cart clear via unitOfWork batch
|
|
447
|
+
* 5. Multi-coupon pro-rating per order group
|
|
448
|
+
* 6. order_placed notifications (buyer + seller)
|
|
449
|
+
* 7. Confirmation email + RTDB success signal (both fire-and-forget)
|
|
450
|
+
*/
|
|
451
|
+
export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
452
|
+
const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, } = input;
|
|
453
|
+
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
454
|
+
const razorpayFeePercent = siteSettings?.commissions?.razorpayFeePercent ?? 5;
|
|
455
|
+
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
456
|
+
const isValid = await verifyPaymentSignatureWithKeys({
|
|
457
|
+
razorpay_order_id,
|
|
458
|
+
razorpay_payment_id,
|
|
459
|
+
razorpay_signature,
|
|
460
|
+
});
|
|
461
|
+
if (!isValid) {
|
|
462
|
+
serverLogger.warn(`Payment signature verification failed for user ${uid}`);
|
|
463
|
+
failedCheckoutRepository
|
|
464
|
+
.logPayment(uid, "signature_mismatch", "HMAC signature invalid", {
|
|
465
|
+
gatewayOrderId: razorpay_order_id,
|
|
466
|
+
gatewayPaymentId: razorpay_payment_id,
|
|
467
|
+
addressId,
|
|
468
|
+
})
|
|
469
|
+
.catch(() => { });
|
|
470
|
+
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PAYMENT_FAILED);
|
|
471
|
+
}
|
|
472
|
+
const cart = await unitOfWork.carts.getOrCreate(uid);
|
|
473
|
+
if (!cart.items || cart.items.length === 0) {
|
|
474
|
+
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
475
|
+
}
|
|
476
|
+
const address = await unitOfWork.addresses.findById(uid, addressId);
|
|
477
|
+
if (!address) {
|
|
478
|
+
throw new NotFoundError(ERROR_MESSAGES.CHECKOUT.ADDRESS_REQUIRED);
|
|
479
|
+
}
|
|
480
|
+
const shippingAddress = formatShippingAddress(address);
|
|
481
|
+
const db = getAdminDb();
|
|
482
|
+
const otpRef = consentOtpRef(db, uid, addressId);
|
|
483
|
+
{
|
|
484
|
+
const otpSnap = await otpRef.get();
|
|
485
|
+
const otpData = otpSnap.exists
|
|
486
|
+
? otpSnap.data()
|
|
487
|
+
: null;
|
|
488
|
+
const isConsentValid = otpData?.verified === true &&
|
|
489
|
+
otpData.expiresAt &&
|
|
490
|
+
(resolveDate(otpData.expiresAt)?.getTime() ?? 0) > Date.now();
|
|
491
|
+
if (!isConsentValid) {
|
|
492
|
+
const reason = !otpData ? "otp_not_verified" : "consent_expired";
|
|
493
|
+
failedCheckoutRepository
|
|
494
|
+
.logPayment(uid, reason, "Consent OTP missing or expired at payment verify time", {
|
|
495
|
+
gatewayOrderId: razorpay_order_id,
|
|
496
|
+
gatewayPaymentId: razorpay_payment_id,
|
|
497
|
+
addressId,
|
|
498
|
+
})
|
|
499
|
+
.catch(() => { });
|
|
500
|
+
throw new ApiError(403, "Order verification required. Please complete OTP verification and retry.");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const productChecks = await Promise.all(cart.items.map(async (item) => {
|
|
504
|
+
const product = await unitOfWork.products.findById(item.productId);
|
|
505
|
+
return { item, product };
|
|
506
|
+
}));
|
|
507
|
+
// SB6-C — maxPerUser cap check before we touch any state.
|
|
508
|
+
await enforceMaxPerUserForCart({
|
|
509
|
+
userId: uid,
|
|
510
|
+
items: productChecks
|
|
511
|
+
.filter((p) => p.product !== null && p.product !== undefined)
|
|
512
|
+
.map(({ item, product }) => ({ item, product })),
|
|
513
|
+
});
|
|
514
|
+
// SB6-C — prize-pool cap. Runs against the freshly-read product snapshots.
|
|
515
|
+
for (const { item, product } of productChecks) {
|
|
516
|
+
if (!product)
|
|
517
|
+
continue;
|
|
518
|
+
enforcePrizePoolCap({
|
|
519
|
+
productSnapshot: product,
|
|
520
|
+
requestedQuantity: item.quantity,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
for (const { item, product } of productChecks) {
|
|
524
|
+
if (!product || product.status !== ProductStatusValues.PUBLISHED) {
|
|
525
|
+
failedCheckoutRepository
|
|
526
|
+
.logPayment(uid, "product_unavailable", `Product ${item.productId} not published`, {
|
|
527
|
+
gatewayOrderId: razorpay_order_id,
|
|
528
|
+
gatewayPaymentId: razorpay_payment_id,
|
|
529
|
+
addressId,
|
|
530
|
+
})
|
|
531
|
+
.catch(() => { });
|
|
532
|
+
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PRODUCT_UNAVAILABLE);
|
|
533
|
+
}
|
|
534
|
+
if (product.availableQuantity < item.quantity) {
|
|
535
|
+
failedCheckoutRepository
|
|
536
|
+
.logPayment(uid, "stock_insufficient", `Product ${item.productId} has ${product.availableQuantity} left, requested ${item.quantity}`, {
|
|
537
|
+
gatewayOrderId: razorpay_order_id,
|
|
538
|
+
gatewayPaymentId: razorpay_payment_id,
|
|
539
|
+
addressId,
|
|
540
|
+
})
|
|
541
|
+
.catch(() => { });
|
|
542
|
+
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.INSUFFICIENT_STOCK);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
{
|
|
546
|
+
const cartSubtotalRs = productChecks.reduce((sum, { item, product }) => sum + product.price * item.quantity, 0);
|
|
547
|
+
const expectedPlatformFee = Math.round(cartSubtotalRs * (razorpayFeePercent / 100) * 100) / 100;
|
|
548
|
+
const expectedPaymentAmountRs = cartSubtotalRs + expectedPlatformFee;
|
|
549
|
+
const rzpOrderRecord = await fetchRazorpayOrder(razorpay_order_id);
|
|
550
|
+
const paidAmountRs = paiseToRupees(rzpOrderRecord.amount);
|
|
551
|
+
if (paidAmountRs < expectedPaymentAmountRs - 1) {
|
|
552
|
+
serverLogger.warn(`Payment amount mismatch for user ${uid}: paid ₹${paidAmountRs}, expected ≥ ₹${expectedPaymentAmountRs}`);
|
|
553
|
+
failedCheckoutRepository
|
|
554
|
+
.logPayment(uid, "amount_mismatch", `Paid ₹${paidAmountRs}, expected ≥ ₹${expectedPaymentAmountRs}`, {
|
|
555
|
+
gatewayOrderId: razorpay_order_id,
|
|
556
|
+
gatewayPaymentId: razorpay_payment_id,
|
|
557
|
+
amountRs: paidAmountRs,
|
|
558
|
+
addressId,
|
|
559
|
+
})
|
|
560
|
+
.catch(() => { });
|
|
561
|
+
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PAYMENT_FAILED);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const appliedCoupons = cart.appliedCoupons ?? [];
|
|
565
|
+
const orderGroups = splitCartIntoOrderGroups(productChecks);
|
|
566
|
+
const orderIds = [];
|
|
567
|
+
let total = 0;
|
|
568
|
+
const emailsToSend = [];
|
|
569
|
+
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s +
|
|
570
|
+
g.reduce((gs, { item, product }) => gs + product.price * item.quantity, 0), 0);
|
|
571
|
+
for (const { items: group, orderType } of orderGroups) {
|
|
572
|
+
const firstItem = group[0].item;
|
|
573
|
+
const groupTotal = group.reduce((sum, { item, product }) => sum + product.price * item.quantity, 0);
|
|
574
|
+
let shippingFee = 0;
|
|
575
|
+
let storeOwnerId;
|
|
576
|
+
const storeId = firstItem.storeId;
|
|
577
|
+
if (storeId) {
|
|
578
|
+
const store = await storeRepository.findById(storeId);
|
|
579
|
+
storeOwnerId = store?.ownerId;
|
|
580
|
+
const sellerUser = storeOwnerId ? await userRepository.findById(storeOwnerId) : null;
|
|
581
|
+
const shippingConfig = sellerUser?.shippingConfig;
|
|
582
|
+
if (shippingConfig?.isConfigured) {
|
|
583
|
+
if (shippingConfig.method === "custom") {
|
|
584
|
+
shippingFee = shippingConfig.customShippingPrice ?? 0;
|
|
585
|
+
}
|
|
586
|
+
else if (shippingConfig.method === "shiprocket") {
|
|
587
|
+
const percentFee = groupTotal * (commissions.platformShippingPercent / 100);
|
|
588
|
+
shippingFee = Math.max(percentFee, commissions.platformShippingFixedMin);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
let couponDiscount = 0;
|
|
593
|
+
const appliedDiscounts = [];
|
|
594
|
+
for (const coupon of appliedCoupons) {
|
|
595
|
+
let couponGroupDiscount = 0;
|
|
596
|
+
const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
|
|
597
|
+
if (isSellerScoped) {
|
|
598
|
+
if (coupon.storeId !== firstItem.storeId)
|
|
599
|
+
continue;
|
|
600
|
+
if (coupon.applicableItemIds?.length) {
|
|
601
|
+
const eligibleTotal = group
|
|
602
|
+
.filter(({ item }) => coupon.applicableItemIds.includes(item.itemId))
|
|
603
|
+
.reduce((s, { item, product }) => s + product.price * item.quantity, 0);
|
|
604
|
+
couponGroupDiscount =
|
|
605
|
+
eligibleTotal > 0
|
|
606
|
+
? Math.min(Math.round((eligibleTotal / groupTotal) * coupon.discountAmount * 100) / 100, eligibleTotal)
|
|
607
|
+
: 0;
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
else if (cartSubtotal > 0) {
|
|
614
|
+
couponGroupDiscount = Math.min(Math.round((groupTotal / cartSubtotal) * coupon.discountAmount * 100) / 100, groupTotal);
|
|
615
|
+
}
|
|
616
|
+
if (couponGroupDiscount > 0) {
|
|
617
|
+
couponDiscount += couponGroupDiscount;
|
|
618
|
+
appliedDiscounts.push({
|
|
619
|
+
code: coupon.code,
|
|
620
|
+
couponId: coupon.couponId,
|
|
621
|
+
type: "coupon",
|
|
622
|
+
discountAmount: couponGroupDiscount,
|
|
623
|
+
scope: coupon.scope,
|
|
624
|
+
storeId: coupon.storeId,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
629
|
+
const platformFee = Math.round(groupTotal * (razorpayFeePercent / 100) * 100) / 100;
|
|
630
|
+
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee;
|
|
631
|
+
total += orderTotal;
|
|
632
|
+
// SB8-F — stamp listingType + reveal-status + deadline for prize-draw lines.
|
|
633
|
+
const groupRevealDeadlineRzp = orderType === "prize-draw" && group[0].product
|
|
634
|
+
? computePrizeRevealDeadline(group[0].product)
|
|
635
|
+
: undefined;
|
|
636
|
+
const orderItems = group.map(({ item, product }) => {
|
|
637
|
+
const isPrizeDrawLine = product?.listingType === "prize-draw";
|
|
638
|
+
return {
|
|
639
|
+
productId: item.productId,
|
|
640
|
+
productTitle: item.productTitle,
|
|
641
|
+
quantity: item.quantity,
|
|
642
|
+
unitPrice: product.price,
|
|
643
|
+
totalPrice: product.price * item.quantity,
|
|
644
|
+
...(isPrizeDrawLine
|
|
645
|
+
? {
|
|
646
|
+
listingType: "prize-draw",
|
|
647
|
+
prizeRevealStatus: product?.prizeRevealStatus === "open"
|
|
648
|
+
? "open"
|
|
649
|
+
: "pending",
|
|
650
|
+
prizeRevealDeadline: groupRevealDeadlineRzp?.toISOString(),
|
|
651
|
+
}
|
|
652
|
+
: product?.listingType
|
|
653
|
+
? { listingType: product.listingType }
|
|
654
|
+
: {}),
|
|
655
|
+
};
|
|
656
|
+
});
|
|
657
|
+
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
658
|
+
const imageUrls = [
|
|
659
|
+
...new Set(group
|
|
660
|
+
.map(({ product }) => product?.mainImage)
|
|
661
|
+
.filter((url) => typeof url === "string" && url.length > 0)),
|
|
662
|
+
];
|
|
663
|
+
// SB-UNI-D — "bundle" order-type removed; bundle cart lines will expand
|
|
664
|
+
// to N product order lines at checkout (forward-looking, not wired).
|
|
665
|
+
const isPrizeDrawOrder = orderType === "prize-draw";
|
|
666
|
+
const prizeDrawFields = isPrizeDrawOrder && group[0].product
|
|
667
|
+
? {
|
|
668
|
+
prizeDrawProductId: group[0].product.id,
|
|
669
|
+
isNonRefundable: true,
|
|
670
|
+
prizeRevealDeadline: computePrizeRevealDeadline(group[0].product),
|
|
671
|
+
}
|
|
672
|
+
: {};
|
|
673
|
+
const order = await unitOfWork.orders.create({
|
|
674
|
+
productId: firstItem.productId,
|
|
675
|
+
productTitle: firstItem.productTitle,
|
|
676
|
+
userId: uid,
|
|
677
|
+
userName,
|
|
678
|
+
userEmail,
|
|
679
|
+
quantity: totalQuantity,
|
|
680
|
+
unitPrice: group[0].product.price,
|
|
681
|
+
totalPrice: orderTotal,
|
|
682
|
+
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
683
|
+
storeId: firstItem.storeId || undefined,
|
|
684
|
+
storeName: firstItem.storeName || undefined,
|
|
685
|
+
items: orderItems,
|
|
686
|
+
orderType,
|
|
687
|
+
offerId: firstItem.offerId ?? undefined,
|
|
688
|
+
status: OrderStatusValues.CONFIRMED,
|
|
689
|
+
paymentStatus: PaymentStatusValues.PAID,
|
|
690
|
+
paymentMethod: PaymentMethodValues.ONLINE,
|
|
691
|
+
paymentId: razorpay_payment_id,
|
|
692
|
+
shippingAddress,
|
|
693
|
+
notes,
|
|
694
|
+
platformFee,
|
|
695
|
+
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
696
|
+
couponCode: appliedDiscounts[0]?.code,
|
|
697
|
+
couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
|
|
698
|
+
appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
|
|
699
|
+
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
|
700
|
+
...prizeDrawFields,
|
|
701
|
+
});
|
|
702
|
+
orderIds.push(order.id);
|
|
703
|
+
if (userEmail) {
|
|
704
|
+
emailsToSend.push({
|
|
705
|
+
to: userEmail,
|
|
706
|
+
userName,
|
|
707
|
+
orderId: order.id,
|
|
708
|
+
productTitle: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
709
|
+
quantity: totalQuantity,
|
|
710
|
+
totalPrice: orderTotal,
|
|
711
|
+
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
712
|
+
shippingAddress,
|
|
713
|
+
paymentMethod: PaymentMethodValues.ONLINE,
|
|
714
|
+
items: orderItems,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
emitOrderPlacedNotifications({
|
|
718
|
+
orderId: order.id,
|
|
719
|
+
buyerUid: uid,
|
|
720
|
+
buyerName: userName,
|
|
721
|
+
storeOwnerId,
|
|
722
|
+
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
723
|
+
paid: true,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
await unitOfWork.runBatch((batch) => {
|
|
727
|
+
for (const { item, product } of productChecks) {
|
|
728
|
+
if (!product)
|
|
729
|
+
continue;
|
|
730
|
+
const prizeBump = product.listingType === "prize-draw"
|
|
731
|
+
? {
|
|
732
|
+
prizeCurrentEntries: (product.prizeCurrentEntries ?? 0) + item.quantity,
|
|
733
|
+
}
|
|
734
|
+
: {};
|
|
735
|
+
unitOfWork.products.updateInBatch(batch, item.productId, {
|
|
736
|
+
availableQuantity: product.availableQuantity - item.quantity,
|
|
737
|
+
...prizeBump,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
unitOfWork.carts.updateInBatch(batch, uid, {
|
|
741
|
+
items: [],
|
|
742
|
+
appliedCoupons: [],
|
|
743
|
+
selectedItemIds: null,
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
otpRef.delete().catch(() => { });
|
|
747
|
+
if (emailsToSend.length > 0) {
|
|
748
|
+
Promise.all(emailsToSend.map((e) => sendOrderConfirmationEmail(e))).catch((err) => serverLogger.error("Order confirmation email error:", err));
|
|
749
|
+
}
|
|
750
|
+
serverLogger.info(`verifyAndPlaceRazorpayOrderAction: ${orderIds.length} order(s) placed for uid=${uid} — payment ${razorpay_payment_id}`);
|
|
751
|
+
getAdminRealtimeDb()
|
|
752
|
+
.ref(`${RTDB_PATHS.PAYMENT_EVENTS}/${razorpay_order_id}`)
|
|
753
|
+
.update({ status: "success", orderIds, updatedAt: Date.now() })
|
|
754
|
+
.catch((err) => serverLogger.warn("Payment event RTDB signal failed (non-critical)", { err }));
|
|
755
|
+
return { orderIds, total, itemCount: orderIds.length };
|
|
756
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createCheckoutOrderAction, attachPaymentAction, type CreateCheckoutOrderInput, } from "./actions";
|
|
1
|
+
export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, type CreateCheckoutOrderInput, type VerifyAndPlaceRazorpayOrderInput, } from "./actions";
|
|
2
2
|
export { formatShippingAddress, type CheckoutOrderResult, } from "./data";
|
|
3
3
|
export { CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, type CheckoutPaymentMethod, } from "../../../shared/features/checkout/config";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createCheckoutOrderAction, attachPaymentAction, } from "./actions";
|
|
1
|
+
export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, } from "./actions";
|
|
2
2
|
export { formatShippingAddress, } from "./data";
|
|
3
3
|
export { CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, } from "../../../shared/features/checkout/config";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prize-draw + bundle + maxPerUser gates for the checkout pipeline (SB6-C, SB8-A).
|
|
3
|
+
*
|
|
4
|
+
* Shared helper used by both `createCheckoutOrderAction` (COD/UPI-manual path)
|
|
5
|
+
* and `verifyAndPlaceRazorpayOrderAction` (Razorpay path). Pulled out here so
|
|
6
|
+
* the two transactional flows enforce the same rules.
|
|
7
|
+
*/
|
|
8
|
+
import type { ProductDocument } from "../../../../features/products/schemas/firestore";
|
|
9
|
+
import type { CartItemDocument } from "../../../../features/cart/schemas/firestore";
|
|
10
|
+
export interface PerUserCapViolation {
|
|
11
|
+
productId: string;
|
|
12
|
+
productTitle: string;
|
|
13
|
+
allowance: number;
|
|
14
|
+
alreadyPurchased: number;
|
|
15
|
+
requested: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pre-transaction read: for every cart item whose product has `maxPerUser`
|
|
19
|
+
* set, count the user's existing active orders and reject the checkout if
|
|
20
|
+
* the new request would push them over the cap.
|
|
21
|
+
*
|
|
22
|
+
* Active = pending / confirmed / processing / shipped / delivered. Cancelled
|
|
23
|
+
* + refunded orders do NOT count (stock was returned to circulation).
|
|
24
|
+
*
|
|
25
|
+
* Throws ValidationError with code `MAX_PER_USER` listing every violating
|
|
26
|
+
* product. The check is informational-rich on purpose — the UI surfaces all
|
|
27
|
+
* blocked items at once instead of revealing them one at a time.
|
|
28
|
+
*/
|
|
29
|
+
export declare function enforceMaxPerUserForCart(args: {
|
|
30
|
+
userId: string;
|
|
31
|
+
items: Array<{
|
|
32
|
+
item: CartItemDocument;
|
|
33
|
+
product: ProductDocument;
|
|
34
|
+
}>;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* SB8-A — compute the deadline by which the buyer must claim their prize
|
|
38
|
+
* via the reveal API.
|
|
39
|
+
*
|
|
40
|
+
* Rules:
|
|
41
|
+
* - If the reveal window has not yet opened:
|
|
42
|
+
* deadline = min(revealWindowStart + revealDeadlineDays, revealWindowEnd)
|
|
43
|
+
* - If the window is already open:
|
|
44
|
+
* deadline = min(now + revealDeadlineDays, revealWindowEnd)
|
|
45
|
+
*
|
|
46
|
+
* Returns `undefined` if the product lacks the prize-draw fields.
|
|
47
|
+
*/
|
|
48
|
+
export declare function computePrizeRevealDeadline(product: Pick<ProductDocument, "prizeRevealWindowStart" | "prizeRevealWindowEnd" | "prizeRevealDeadlineDays">, now?: Date): Date | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Inside the existing checkout transaction, validate the prize-pool cap for
|
|
51
|
+
* each prize-draw line. Caller must pass the fresh product snapshot read
|
|
52
|
+
* within the transaction (so `prizeCurrentEntries` is up-to-date).
|
|
53
|
+
*
|
|
54
|
+
* Throws ValidationError if any item would overflow the pool.
|
|
55
|
+
*/
|
|
56
|
+
export declare function enforcePrizePoolCap(args: {
|
|
57
|
+
productSnapshot: ProductDocument;
|
|
58
|
+
requestedQuantity: number;
|
|
59
|
+
}): void;
|