@mohasinac/appkit 4.1.3 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/auth/actions.d.ts +0 -1
- package/dist/_internal/server/features/auth/actions.js +0 -1
- package/dist/_internal/server/features/bundles/data.d.ts +22 -4
- package/dist/_internal/server/features/bundles/data.js +41 -4
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.d.ts +15 -0
- package/dist/_internal/server/features/checkout/actions.js +249 -257
- package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
- package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
- package/dist/_internal/server/features/orders/adapters.js +1 -14
- package/dist/_internal/server/features/products/actions.js +8 -3
- package/dist/_internal/server/features/products/service.d.ts +15 -0
- package/dist/_internal/server/features/products/service.js +41 -0
- package/dist/_internal/server/functions/firestore.d.ts +3 -1
- package/dist/_internal/server/functions/firestore.js +17 -1
- package/dist/_internal/server/functions/scheduled.d.ts +3 -5
- package/dist/_internal/server/functions/scheduled.js +14 -30
- package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
- package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
- package/dist/_internal/server/jobs/core/index.d.ts +4 -4
- package/dist/_internal/server/jobs/core/index.js +4 -4
- package/dist/_internal/server/jobs/core/jobRunners.js +10 -0
- package/dist/_internal/server/jobs/core/newsletterExport.d.ts +15 -0
- package/dist/_internal/server/jobs/core/newsletterExport.js +43 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +3 -2
- package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
- package/dist/_internal/server/jobs/core/revenueRollup.d.ts +13 -0
- package/dist/_internal/server/jobs/core/revenueRollup.js +22 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +82 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +4 -4
- package/dist/_internal/server/jobs/handlers/index.js +9 -6
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
- package/dist/_internal/server/jobs/handlers/revenueRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/revenueRollup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +40 -10
- package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
- package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
- package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
- package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
- package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
- package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
- package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +7 -0
- package/dist/_internal/shared/features/checkout/config.js +7 -0
- package/dist/_internal/shared/fees/calculator.d.ts +26 -0
- package/dist/_internal/shared/fees/calculator.js +25 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +5 -5
- package/dist/constants/field-names.d.ts +17 -1
- package/dist/constants/field-names.js +17 -1
- package/dist/features/about/components/FeesView.js +18 -0
- package/dist/features/account/hooks/useNotifications.js +5 -0
- package/dist/features/admin/actions/admin-actions.js +6 -0
- package/dist/features/admin/actions/notification-actions.d.ts +4 -1
- package/dist/features/admin/actions/notification-actions.js +57 -12
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
- package/dist/features/admin/components/AdminNewsletterView.js +42 -11
- package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +115 -11
- package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
- package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
- package/dist/features/admin/hooks/useAdminListing.js +4 -2
- package/dist/features/admin/repository/analytics-rollup.repository.d.ts +25 -0
- package/dist/features/admin/repository/analytics-rollup.repository.js +31 -0
- package/dist/features/admin/repository/site-settings.repository.js +8 -1
- package/dist/features/admin/schemas/firestore.d.ts +54 -5
- package/dist/features/admin/schemas/firestore.js +63 -26
- package/dist/features/auctions/actions/bid-actions.js +3 -1
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
- package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
- package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
- package/dist/features/auctions/components/CollapsibleBidHistory.js +21 -3
- package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
- package/dist/features/auctions/components/LiveBidPrice.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
- package/dist/features/auctions/hooks/useBids.d.ts +33 -0
- package/dist/features/auctions/hooks/useBids.js +32 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
- package/dist/features/auth/checkout-value-otp.d.ts +7 -10
- package/dist/features/auth/checkout-value-otp.js +14 -10
- package/dist/features/auth/server.d.ts +0 -1
- package/dist/features/auth/server.js +0 -1
- package/dist/features/blog/api/[slug]/route.d.ts +5 -0
- package/dist/features/blog/api/[slug]/route.js +12 -3
- package/dist/features/blog/components/BlogPostView.d.ts +2 -0
- package/dist/features/blog/components/BlogPostView.js +5 -4
- package/dist/features/blog/hooks/useBlog.d.ts +2 -0
- package/dist/features/blog/hooks/useBlog.js +2 -0
- package/dist/features/blog/repository/blog.repository.d.ts +7 -0
- package/dist/features/blog/repository/blog.repository.js +44 -0
- package/dist/features/blog/schemas/firestore.d.ts +3 -0
- package/dist/features/blog/schemas/firestore.js +1 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/index.d.ts +0 -2
- package/dist/features/cart/components/index.js +0 -1
- package/dist/features/cart/hooks/useCartCount.js +22 -5
- package/dist/features/cart/hooks/useGuestCart.js +11 -1
- package/dist/features/cart/utils/guest-cart.d.ts +4 -0
- package/dist/features/cart/utils/guest-cart.js +10 -0
- package/dist/features/cart/utils/pending-ops.d.ts +17 -0
- package/dist/features/cart/utils/pending-ops.js +46 -5
- package/dist/features/categories/components/BrandDetailPageView.js +6 -2
- package/dist/features/categories/components/BundleDetailView.js +4 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
- package/dist/features/categories/schemas/firestore.d.ts +7 -0
- package/dist/features/checkout/actions/index.d.ts +0 -1
- package/dist/features/checkout/actions/index.js +0 -1
- package/dist/features/checkout/schemas/firestore.d.ts +2 -2
- package/dist/features/events/actions/event-actions.d.ts +2 -0
- package/dist/features/events/actions/event-actions.js +5 -0
- package/dist/features/events/components/EventCard.js +1 -2
- package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
- package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
- package/dist/features/events/components/index.d.ts +2 -0
- package/dist/features/events/components/index.js +1 -0
- package/dist/features/events/repository/events.repository.d.ts +6 -0
- package/dist/features/events/repository/events.repository.js +21 -0
- package/dist/features/history/repository/user-history.repository.d.ts +1 -1
- package/dist/features/history/utils/guest-history.d.ts +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
- package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/layout/BackToTop.d.ts +9 -5
- package/dist/features/layout/BackToTop.js +17 -21
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.d.ts +7 -0
- package/dist/features/media/MediaVideo.js +36 -5
- package/dist/features/orders/actions/refund-actions.js +1 -0
- package/dist/features/orders/components/OrdersList.js +1 -5
- package/dist/features/orders/schemas/firestore.d.ts +16 -8
- package/dist/features/orders/schemas/index.d.ts +3 -32
- package/dist/features/orders/schemas/index.js +1 -4
- package/dist/features/orders/types/index.d.ts +6 -9
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
- package/dist/features/products/actions/product-actions.js +13 -10
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
- package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
- package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
- package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
- package/dist/features/products/components/PrizeRevealModal.js +18 -147
- package/dist/features/products/components/ProductDetailPageView.js +72 -30
- package/dist/features/products/components/ProductForm.js +7 -14
- package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
- package/dist/features/products/components/ProductGalleryClient.js +29 -10
- package/dist/features/products/components/ProductsIndexListing.js +2 -1
- package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +1 -1
- package/dist/features/products/components/index.d.ts +4 -2
- package/dist/features/products/components/index.js +1 -0
- package/dist/features/products/constants/action-defs.d.ts +28 -4
- package/dist/features/products/constants/action-defs.js +76 -3
- package/dist/features/products/repository/products.repository.d.ts +6 -0
- package/dist/features/products/repository/products.repository.js +20 -1
- package/dist/features/products/schemas/firestore.d.ts +26 -2
- package/dist/features/products/schemas/index.d.ts +11 -3
- package/dist/features/products/schemas/index.js +10 -2
- package/dist/features/products/types/index.d.ts +2 -1
- package/dist/features/reviews/actions/review-actions.js +1 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
- package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
- package/dist/features/reviews/components/ReviewsList.js +2 -1
- package/dist/features/reviews/schemas/firestore.d.ts +3 -0
- package/dist/features/reviews/schemas/index.d.ts +6 -0
- package/dist/features/reviews/schemas/index.js +2 -0
- package/dist/features/reviews/types/index.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.js +3 -2
- package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
- package/dist/features/scams/repository/scammer.repository.js +24 -0
- package/dist/features/seller/actions/seller-actions.js +8 -0
- package/dist/features/seller/components/SellerOrdersView.js +1 -1
- package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
- package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
- package/dist/features/stores/components/StoreNavTabs.js +21 -9
- package/dist/features/tester/actions/index.d.ts +1 -0
- package/dist/features/tester/actions/index.js +1 -0
- package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
- package/dist/features/tester/actions/leaderboard-actions.js +5 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
- package/dist/features/tester/components/index.d.ts +2 -0
- package/dist/features/tester/components/index.js +1 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
- package/dist/features/tester/schemas/firestore.d.ts +22 -1
- package/dist/features/tester/schemas/firestore.js +8 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
- package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
- package/dist/features/tester/seed-data/products-tester-seed-data.js +152 -8
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +249 -14
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +10 -1
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +47 -0
- package/dist/features/whatsapp-bot/types/index.d.ts +19 -0
- package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
- package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
- package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
- package/dist/index.d.ts +23 -24
- package/dist/index.js +29 -60
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +2 -2
- package/dist/next/routing/route-map.d.ts +4 -0
- package/dist/next/routing/route-map.js +2 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +6 -26
- package/dist/seed/bids-seed-data.js +14 -2
- package/dist/seed/blog-posts-seed-data.js +2 -1
- package/dist/seed/categories-seed-data.js +11 -2
- package/dist/seed/events-seed-data.js +11 -11
- package/dist/seed/faq-seed-data.js +5 -1
- package/dist/seed/products-auctions-seed-data.js +2 -2
- package/dist/seed/products-preorders-seed-data.d.ts +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +56 -10
- package/dist/seed/products-standard-seed-data.js +82 -3
- package/dist/seed/scammers-seed-data.js +75 -2
- package/dist/seed/site-settings-seed-data.js +12 -2
- package/dist/server-entry.d.ts +0 -1
- package/dist/server-entry.js +0 -1
- package/dist/server.d.ts +6 -22
- package/dist/server.js +12 -59
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.js +1 -1
- package/dist/ui/components/ImageLightbox.d.ts +9 -0
- package/dist/ui/components/ImageLightbox.js +6 -3
- package/dist/ui/components/ListingToolbar.d.ts +18 -1
- package/dist/ui/components/ListingToolbar.js +5 -2
- package/dist/ui/components/StickyToolbar.js +13 -8
- package/package.json +1 -1
|
@@ -1,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).
|
|
@@ -12,27 +11,25 @@ import { roundRupees } from "../../../../utils/number.formatter";
|
|
|
12
11
|
import { ApiError, ValidationError, NotFoundError, ERROR_MESSAGES } from "../../../../errors";
|
|
13
12
|
import { ORDER_FIELDS } from "../../../../constants/field-names";
|
|
14
13
|
import { serverLogger } from "../../../../monitoring";
|
|
15
|
-
import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository,
|
|
14
|
+
import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, } from "../../../../repositories";
|
|
16
15
|
import { calculateGst } from "../../../shared/fees/calculator";
|
|
17
16
|
import { computePreOrderDepositAmount } from "../../../shared/checkout/order-math";
|
|
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 {
|
|
22
|
-
import { computeCodHandlingFee } from "../../../shared/fees/calculator";
|
|
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";
|
|
@@ -112,9 +109,8 @@ function assertLiveJurisdiction(cartItemsArg, productByIdArg, buyerState) {
|
|
|
112
109
|
* either party. We emit the buyer + seller rows here at the create boundary.
|
|
113
110
|
*/
|
|
114
111
|
function emitOrderPlacedNotifications(args) {
|
|
115
|
-
const { orderId, buyerUid, buyerName, storeOwnerId, productLabel, paid } = args;
|
|
116
|
-
const buyerNotif =
|
|
117
|
-
.create({
|
|
112
|
+
const { orderId, buyerUid, buyerName, storeOwnerId, productLabel, paid, whatsappNotifyAddon } = args;
|
|
113
|
+
const buyerNotif = sendNotification({
|
|
118
114
|
userId: buyerUid,
|
|
119
115
|
type: "order_placed",
|
|
120
116
|
priority: "normal",
|
|
@@ -123,14 +119,13 @@ function emitOrderPlacedNotifications(args) {
|
|
|
123
119
|
relatedId: orderId,
|
|
124
120
|
relatedType: "order",
|
|
125
121
|
actionUrl: `/user/orders/view/${orderId}`,
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
orderWhatsappAddonPaid: whatsappNotifyAddon,
|
|
123
|
+
}).catch((err) => serverLogger.warn("Failed to send buyer order_placed notification", {
|
|
128
124
|
err: err instanceof Error ? err.message : String(err),
|
|
129
125
|
orderId,
|
|
130
126
|
}));
|
|
131
127
|
const sellerNotif = storeOwnerId
|
|
132
|
-
?
|
|
133
|
-
.create({
|
|
128
|
+
? sendNotification({
|
|
134
129
|
userId: storeOwnerId,
|
|
135
130
|
type: "order_placed",
|
|
136
131
|
priority: "high",
|
|
@@ -139,8 +134,7 @@ function emitOrderPlacedNotifications(args) {
|
|
|
139
134
|
relatedId: orderId,
|
|
140
135
|
relatedType: "order",
|
|
141
136
|
actionUrl: `/store/orders/${orderId}/view`,
|
|
142
|
-
})
|
|
143
|
-
.catch((err) => serverLogger.warn("Failed to create seller order_placed notification", {
|
|
137
|
+
}).catch((err) => serverLogger.warn("Failed to send seller order_placed notification", {
|
|
144
138
|
err: err instanceof Error ? err.message : String(err),
|
|
145
139
|
orderId,
|
|
146
140
|
}))
|
|
@@ -218,18 +212,6 @@ async function flushCouponUsageAccumulator(accumulator, uid) {
|
|
|
218
212
|
serverLogger.error("Failed to record coupon usage:", { err: err instanceof Error ? err.message : String(err) });
|
|
219
213
|
}
|
|
220
214
|
}
|
|
221
|
-
function grantConsentOtpBypassCredit(db, uid, emailOtpUsed, unavailableCount) {
|
|
222
|
-
if (!emailOtpUsed || unavailableCount === 0)
|
|
223
|
-
return;
|
|
224
|
-
const metaRef = consentOtpRateLimitRef(db, uid);
|
|
225
|
-
metaRef
|
|
226
|
-
.get()
|
|
227
|
-
.then((snap) => {
|
|
228
|
-
const cur = snap.exists ? (snap.data()?.bypassCredits ?? 0) : 0;
|
|
229
|
-
return metaRef.set({ bypassCredits: Math.min(cur + 1, CONSENT_OTP_MAX_BYPASS_CREDITS) }, { merge: true });
|
|
230
|
-
})
|
|
231
|
-
.catch((err) => serverLogger.warn("Failed to grant consent OTP bypass credit", { err }));
|
|
232
|
-
}
|
|
233
215
|
function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
234
216
|
if (emailsToSend.length === 0)
|
|
235
217
|
return;
|
|
@@ -251,7 +233,7 @@ function unitPriceFor(item, product) {
|
|
|
251
233
|
* so the caller can accumulate the checkout-wide `total`.
|
|
252
234
|
*/
|
|
253
235
|
async function createOrderForGroup(group, orderType, ctx) {
|
|
254
|
-
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, } = ctx;
|
|
236
|
+
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, whatsappNotifyAddon, giftWrapAddon, giftWrapMessage, shipmentProtectionAddon, } = ctx;
|
|
255
237
|
const firstItem = group[0].item;
|
|
256
238
|
const firstProduct = group[0].product;
|
|
257
239
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
@@ -327,6 +309,9 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
327
309
|
// Handling fee charged to the buyer for choosing COD, on top of the order
|
|
328
310
|
// total — not deducted from the deposit/remaining split above.
|
|
329
311
|
const codHandlingFee = paymentMethod === "cod" ? computeCodHandlingFee(groupTotal, commissions) : 0;
|
|
312
|
+
const whatsappNotifyFee = computeWhatsAppNotifyFee(whatsappNotifyAddon, commissions);
|
|
313
|
+
const giftWrapFee = computeGiftWrapFee(giftWrapAddon, commissions);
|
|
314
|
+
const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, shipmentProtectionAddon, commissions);
|
|
330
315
|
let couponDiscount = 0;
|
|
331
316
|
const appliedDiscounts = [];
|
|
332
317
|
const groupCouponCodes = new Set();
|
|
@@ -371,7 +356,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
371
356
|
}
|
|
372
357
|
}
|
|
373
358
|
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
374
|
-
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + (emiSchedule?.surchargeAmount ?? 0) + (gstBreakdown?.gstAmount ?? 0);
|
|
359
|
+
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + (emiSchedule?.surchargeAmount ?? 0) + (gstBreakdown?.gstAmount ?? 0);
|
|
375
360
|
const imageUrls = [
|
|
376
361
|
...new Set(group
|
|
377
362
|
.map(({ product }) => product.mainImage)
|
|
@@ -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
|
|
@@ -428,6 +410,13 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
428
410
|
depositAmount: adminBypass ? undefined : depositAmount,
|
|
429
411
|
codRemainingAmount: adminBypass ? undefined : codRemainingAmount,
|
|
430
412
|
codHandlingFee: !adminBypass && codHandlingFee > 0 ? codHandlingFee : undefined,
|
|
413
|
+
whatsappNotifyAddon: !adminBypass && whatsappNotifyFee > 0 ? true : undefined,
|
|
414
|
+
whatsappNotifyFee: !adminBypass && whatsappNotifyFee > 0 ? whatsappNotifyFee : undefined,
|
|
415
|
+
giftWrapAddon: !adminBypass && giftWrapFee > 0 ? true : undefined,
|
|
416
|
+
giftWrapFee: !adminBypass && giftWrapFee > 0 ? giftWrapFee : undefined,
|
|
417
|
+
giftWrapMessage: !adminBypass && giftWrapFee > 0 ? giftWrapMessage?.slice(0, 500) : undefined,
|
|
418
|
+
shipmentProtectionAddon: !adminBypass && shipmentProtectionFee > 0 ? true : undefined,
|
|
419
|
+
shipmentProtectionFee: !adminBypass && shipmentProtectionFee > 0 ? shipmentProtectionFee : undefined,
|
|
431
420
|
taxableAmount: gstBreakdown?.taxableAmount,
|
|
432
421
|
gstAmount: gstBreakdown?.gstAmount,
|
|
433
422
|
cgst: gstBreakdown?.cgst || undefined,
|
|
@@ -486,6 +475,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
486
475
|
storeOwnerId,
|
|
487
476
|
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
488
477
|
paid: adminBypass,
|
|
478
|
+
whatsappNotifyAddon: !adminBypass && whatsappNotifyFee > 0,
|
|
489
479
|
});
|
|
490
480
|
return groupTotal;
|
|
491
481
|
}
|
|
@@ -496,7 +486,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
496
486
|
* auth + rate-limiting before calling this and supply the resolved user fields.
|
|
497
487
|
*/
|
|
498
488
|
export async function createCheckoutOrderAction(input) {
|
|
499
|
-
const { userId: uid, userName, userEmail, addressId, paymentMethod, emiTenureMonths, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, outOfStockPolicy = OutOfStockPolicyValues.SKIP_ITEMS, } = input;
|
|
489
|
+
const { userId: uid, userName, userEmail, addressId, paymentMethod, emiTenureMonths, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, outOfStockPolicy = OutOfStockPolicyValues.SKIP_ITEMS, whatsappNotifyAddon = false, giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, } = input;
|
|
500
490
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
501
491
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
502
492
|
const emiSettings = siteSettings?.emi ?? CHECKOUT_DEFAULT_EMI_SETTINGS;
|
|
@@ -569,7 +559,6 @@ export async function createCheckoutOrderAction(input) {
|
|
|
569
559
|
shippingAddress = formatShippingAddress(resolvedAddress);
|
|
570
560
|
}
|
|
571
561
|
const db = getAdminDb();
|
|
572
|
-
const otpRef = isDigitalCart ? null : consentOtpRef(db, uid, addressId);
|
|
573
562
|
// SB6-C — pre-tx fetch products so we can run the maxPerUser cap check
|
|
574
563
|
// (count queries can't run inside a Firestore transaction). The same
|
|
575
564
|
// product docs are re-read inside the transaction below for stock + prize
|
|
@@ -605,21 +594,6 @@ export async function createCheckoutOrderAction(input) {
|
|
|
605
594
|
let stockResult;
|
|
606
595
|
try {
|
|
607
596
|
stockResult = await db.runTransaction(async (tx) => {
|
|
608
|
-
let emailOtpUsed = false;
|
|
609
|
-
if (!isDigitalCart && otpRef) {
|
|
610
|
-
const otpSnap = await tx.get(otpRef);
|
|
611
|
-
const otpData = otpSnap.exists
|
|
612
|
-
? otpSnap.data()
|
|
613
|
-
: null;
|
|
614
|
-
const isConsentValid = otpData?.verified === true &&
|
|
615
|
-
otpData.expiresAt &&
|
|
616
|
-
(resolveDate(otpData.expiresAt)?.getTime() ?? 0) > Date.now();
|
|
617
|
-
if (!isConsentValid) {
|
|
618
|
-
const reason = !otpData ? "otp_not_verified" : "consent_expired";
|
|
619
|
-
throw Object.assign(new ApiError(403, "Order verification required. Please complete OTP verification before placing this order."), { _failReason: reason });
|
|
620
|
-
}
|
|
621
|
-
emailOtpUsed = otpData.verifiedVia !== "sms" && otpData.verifiedVia !== "admin_bypass";
|
|
622
|
-
}
|
|
623
597
|
// SB-UNI-5 2026-05-13 — bundle-aware stock fan-out. Build the unique
|
|
624
598
|
// product-id set across the whole cart (regular items + each bundle's
|
|
625
599
|
// members) and fetch each one ONCE. Validation walks per cart-line and
|
|
@@ -654,7 +628,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
654
628
|
// S-SBUNI-RULES 2026-05-13 — sync preflight (prize-pool cap, pre-order
|
|
655
629
|
// quota) via rule registry. Bundles skip — they bypass the cart path.
|
|
656
630
|
if (!item.bundleProductIds?.length) {
|
|
657
|
-
runSyncPreflight([{ item, product }]);
|
|
631
|
+
runSyncPreflight([{ item, product }], paymentMethod);
|
|
658
632
|
}
|
|
659
633
|
availableItems.push({ item, product });
|
|
660
634
|
}
|
|
@@ -679,29 +653,21 @@ export async function createCheckoutOrderAction(input) {
|
|
|
679
653
|
selectedItemIds: null,
|
|
680
654
|
updatedAt: new Date(),
|
|
681
655
|
}, { merge: true });
|
|
682
|
-
if (otpRef) {
|
|
683
|
-
tx.delete(otpRef);
|
|
684
|
-
}
|
|
685
656
|
}
|
|
686
|
-
return { available: availableItems, unavailable: unavailableItems
|
|
657
|
+
return { available: availableItems, unavailable: unavailableItems };
|
|
687
658
|
});
|
|
688
659
|
}
|
|
689
660
|
catch (err) {
|
|
690
661
|
void normalizeError(err);
|
|
691
|
-
const reason = err?._failReason === "consent_expired"
|
|
692
|
-
? "consent_expired"
|
|
693
|
-
: err?._failReason === "otp_not_verified"
|
|
694
|
-
? "otp_not_verified"
|
|
695
|
-
: "unknown";
|
|
696
662
|
failedCheckoutRepository
|
|
697
|
-
.logCheckout(uid,
|
|
663
|
+
.logCheckout(uid, "unknown", err instanceof Error ? err.message : String(err), {
|
|
698
664
|
addressId,
|
|
699
665
|
paymentMethod,
|
|
700
666
|
})
|
|
701
667
|
.catch((logErr) => { void normalizeError(logErr); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: logErr instanceof Error ? logErr.message : String(logErr) }); });
|
|
702
668
|
throw err;
|
|
703
669
|
}
|
|
704
|
-
const { available, unavailable
|
|
670
|
+
const { available, unavailable } = stockResult;
|
|
705
671
|
if (available.length === 0) {
|
|
706
672
|
failedCheckoutRepository
|
|
707
673
|
.logCheckout(uid, "stock_failed", "All items out of stock", {
|
|
@@ -747,12 +713,15 @@ export async function createCheckoutOrderAction(input) {
|
|
|
747
713
|
buyerState: resolvedAddress?.state,
|
|
748
714
|
gstSettings: siteSettings?.gst,
|
|
749
715
|
siteContactUpiVpa: siteSettings?.contact?.upiVpa,
|
|
716
|
+
whatsappNotifyAddon,
|
|
717
|
+
giftWrapAddon,
|
|
718
|
+
giftWrapMessage,
|
|
719
|
+
shipmentProtectionAddon,
|
|
750
720
|
};
|
|
751
721
|
for (const { items: group, orderType } of orderGroups) {
|
|
752
722
|
total += await createOrderForGroup(group, orderType, groupCtx);
|
|
753
723
|
}
|
|
754
724
|
await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
|
|
755
|
-
grantConsentOtpBypassCredit(db, uid, emailOtpUsed, unavailable.length);
|
|
756
725
|
dispatchOrderConfirmationEmails(emailsToSend);
|
|
757
726
|
serverLogger.info(`createCheckoutOrderAction: ${orderIds.length} order(s) placed for uid=${uid}` +
|
|
758
727
|
(unavailable.length > 0 ? ` (${unavailable.length} item(s) unavailable)` : ""));
|
|
@@ -872,11 +841,209 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
872
841
|
}
|
|
873
842
|
}
|
|
874
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Places one order for one seller-group within `verifyAndPlaceRazorpayOrderAction`
|
|
846
|
+
* — mirrors `createOrderForGroup`'s role on the COD/cash/EMI path (same
|
|
847
|
+
* per-group fee/coupon/order-doc/notification shape), extracted into its own
|
|
848
|
+
* function purely to keep the parent function under the LARGE_COMPONENT
|
|
849
|
+
* audit threshold (`scripts/audit-code-quality.mjs`). No behavior change
|
|
850
|
+
* from the inline version this replaced.
|
|
851
|
+
*/
|
|
852
|
+
async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
853
|
+
const { appliedCoupons, cartSubtotal, platformFeePercent, gstPercent, whatsappNotifyFee, giftWrapFee, giftWrapMessage, shipmentProtectionAddon, siteSettings, uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, } = ctx;
|
|
854
|
+
// SB-UNI-5 2026-05-13 — bundle cart-lines use item.price (locked bundle
|
|
855
|
+
// price); regular lines use product.price. The helper keeps the math
|
|
856
|
+
// local to this block.
|
|
857
|
+
const unitPriceFor = (item, product) => item.bundleCategorySlug && item.bundleProductIds?.length
|
|
858
|
+
? item.price
|
|
859
|
+
: product.price;
|
|
860
|
+
const firstItem = group[0].item;
|
|
861
|
+
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
862
|
+
// Reuses the same store/seller lookup as the COD/UPI path above instead
|
|
863
|
+
// of re-implementing it inline — was two sequential findById calls per
|
|
864
|
+
// seller group here, duplicated from resolveShippingCost.
|
|
865
|
+
const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId);
|
|
866
|
+
let couponDiscount = 0;
|
|
867
|
+
const appliedDiscounts = [];
|
|
868
|
+
for (const coupon of appliedCoupons) {
|
|
869
|
+
let couponGroupDiscount = 0;
|
|
870
|
+
const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
|
|
871
|
+
if (isSellerScoped) {
|
|
872
|
+
if (coupon.storeId !== firstItem.storeId)
|
|
873
|
+
continue;
|
|
874
|
+
if (coupon.applicableItemIds?.length) {
|
|
875
|
+
const eligibleTotal = group
|
|
876
|
+
.filter(({ item }) => coupon.applicableItemIds.includes(item.itemId))
|
|
877
|
+
.reduce((s, { item, product }) => s + unitPriceFor(item, product) * item.quantity, 0);
|
|
878
|
+
couponGroupDiscount =
|
|
879
|
+
eligibleTotal > 0
|
|
880
|
+
? Math.min(roundRupees((eligibleTotal / groupTotal) * coupon.discountAmount), eligibleTotal)
|
|
881
|
+
: 0;
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
else if (cartSubtotal > 0) {
|
|
888
|
+
couponGroupDiscount = Math.min(roundRupees((groupTotal / cartSubtotal) * coupon.discountAmount), groupTotal);
|
|
889
|
+
}
|
|
890
|
+
if (couponGroupDiscount > 0) {
|
|
891
|
+
couponDiscount += couponGroupDiscount;
|
|
892
|
+
appliedDiscounts.push({
|
|
893
|
+
code: coupon.code,
|
|
894
|
+
couponId: coupon.couponId,
|
|
895
|
+
type: "coupon",
|
|
896
|
+
discountAmount: couponGroupDiscount,
|
|
897
|
+
scope: coupon.scope,
|
|
898
|
+
storeId: coupon.storeId,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
903
|
+
const rawPlatformFee = roundRupees(groupTotal * (platformFeePercent / 100));
|
|
904
|
+
const gstOnFee = roundRupees(rawPlatformFee * (gstPercent / 100));
|
|
905
|
+
const platformFee = rawPlatformFee + gstOnFee;
|
|
906
|
+
const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, shipmentProtectionAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
907
|
+
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee;
|
|
908
|
+
// P-8 GST — deliberately NOT wired into this Razorpay-verify path. The
|
|
909
|
+
// amount-mismatch check above (expectedPaymentAmountRs) compares against
|
|
910
|
+
// what the buyer already paid via the Razorpay order created earlier in
|
|
911
|
+
// the flow; adding product GST here without also adding it to that
|
|
912
|
+
// upstream pre-payment amount calculation would either fail the mismatch
|
|
913
|
+
// check or silently under/over-charge. Wiring GST through the full
|
|
914
|
+
// Razorpay create→verify round-trip is separate follow-up work, tracked
|
|
915
|
+
// alongside P-13 (Razorpay is disabled by default today, so this order
|
|
916
|
+
// type doesn't currently carry a GST breakdown).
|
|
917
|
+
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
918
|
+
const orderItems = group.map(({ item, product }) => {
|
|
919
|
+
const lt = (product?.listingType ?? "standard");
|
|
920
|
+
const itemRule = getListingRule(lt);
|
|
921
|
+
// SB-UNI-5 2026-05-13 — bundle cart-lines surface the locked
|
|
922
|
+
// bundlePrice (item.price), not the representative member's
|
|
923
|
+
// product.price. Stock decrement still runs per member elsewhere.
|
|
924
|
+
const isBundle = Boolean(item.bundleCategorySlug && item.bundleProductIds?.length);
|
|
925
|
+
const unitPrice = isBundle ? item.price : product.price;
|
|
926
|
+
const bundleFields = isBundle
|
|
927
|
+
? {
|
|
928
|
+
bundleCategorySlug: item.bundleCategorySlug,
|
|
929
|
+
bundleProductIds: item.bundleProductIds,
|
|
930
|
+
}
|
|
931
|
+
: {};
|
|
932
|
+
const baseLine = {
|
|
933
|
+
productId: item.productId,
|
|
934
|
+
productTitle: item.productTitle,
|
|
935
|
+
quantity: item.quantity,
|
|
936
|
+
unitPrice,
|
|
937
|
+
totalPrice: unitPrice * item.quantity,
|
|
938
|
+
...bundleFields,
|
|
939
|
+
};
|
|
940
|
+
return itemRule.decorateOrderItem(baseLine, product);
|
|
941
|
+
});
|
|
942
|
+
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
943
|
+
const imageUrls = [
|
|
944
|
+
...new Set(group
|
|
945
|
+
.map(({ product }) => product?.mainImage)
|
|
946
|
+
.filter((url) => typeof url === "string" && url.length > 0)),
|
|
947
|
+
];
|
|
948
|
+
// S-SBUNI-RULES 2026-05-13 — order-doc decoration via rule registry.
|
|
949
|
+
const lt0Rzp = (group[0].product?.listingType ?? "standard");
|
|
950
|
+
const groupRuleRzp = getListingRule(lt0Rzp);
|
|
951
|
+
const extraOrderFields = {
|
|
952
|
+
...groupRuleRzp.decorateOrderDoc(group[0].item, group[0].product),
|
|
953
|
+
};
|
|
954
|
+
const order = await unitOfWork.orders.create({
|
|
955
|
+
productId: firstItem.productId,
|
|
956
|
+
productTitle: firstItem.productTitle,
|
|
957
|
+
userId: uid,
|
|
958
|
+
userName,
|
|
959
|
+
userEmail,
|
|
960
|
+
quantity: totalQuantity,
|
|
961
|
+
unitPrice: group[0].product.price,
|
|
962
|
+
totalPrice: orderTotal,
|
|
963
|
+
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
964
|
+
storeId: firstItem.storeId || undefined,
|
|
965
|
+
storeName: firstItem.storeName || undefined,
|
|
966
|
+
items: orderItems,
|
|
967
|
+
orderType,
|
|
968
|
+
offerId: firstItem.offerId ?? undefined,
|
|
969
|
+
status: OrderStatusValues.CONFIRMED,
|
|
970
|
+
paymentStatus: PaymentStatusValues.PAID,
|
|
971
|
+
paymentMethod: PaymentMethodValues.ONLINE,
|
|
972
|
+
paymentId: razorpay_payment_id,
|
|
973
|
+
paymentRecord: {
|
|
974
|
+
method: "razorpay",
|
|
975
|
+
transactionId: razorpay_payment_id,
|
|
976
|
+
amount: orderTotal,
|
|
977
|
+
paidAt: new Date(),
|
|
978
|
+
verifiedBy: "razorpay-webhook",
|
|
979
|
+
verificationMethod: "webhook",
|
|
980
|
+
gatewayRef: {
|
|
981
|
+
orderId: razorpay_order_id,
|
|
982
|
+
paymentId: razorpay_payment_id,
|
|
983
|
+
signature: razorpay_signature,
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
shippingAddress,
|
|
987
|
+
notes,
|
|
988
|
+
platformFee,
|
|
989
|
+
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
990
|
+
whatsappNotifyAddon: whatsappNotifyFee > 0 ? true : undefined,
|
|
991
|
+
whatsappNotifyFee: whatsappNotifyFee > 0 ? whatsappNotifyFee : undefined,
|
|
992
|
+
giftWrapAddon: giftWrapFee > 0 ? true : undefined,
|
|
993
|
+
giftWrapFee: giftWrapFee > 0 ? giftWrapFee : undefined,
|
|
994
|
+
giftWrapMessage: giftWrapFee > 0 ? giftWrapMessage?.slice(0, 500) : undefined,
|
|
995
|
+
shipmentProtectionAddon: shipmentProtectionFee > 0 ? true : undefined,
|
|
996
|
+
shipmentProtectionFee: shipmentProtectionFee > 0 ? shipmentProtectionFee : undefined,
|
|
997
|
+
couponCode: appliedDiscounts[0]?.code,
|
|
998
|
+
couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
|
|
999
|
+
appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
|
|
1000
|
+
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
|
1001
|
+
outOfStockPolicy,
|
|
1002
|
+
droppedItems: unavailablePaid.length > 0 ? unavailablePaid : undefined,
|
|
1003
|
+
...extraOrderFields,
|
|
1004
|
+
});
|
|
1005
|
+
orderIds.push(order.id);
|
|
1006
|
+
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
1007
|
+
// order is already persisted so a claim failure is logged, not thrown).
|
|
1008
|
+
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
1009
|
+
claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid, {
|
|
1010
|
+
userEmail: userEmail || undefined,
|
|
1011
|
+
userName,
|
|
1012
|
+
productTitle: firstItem.productTitle,
|
|
1013
|
+
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
1014
|
+
}
|
|
1015
|
+
if (userEmail) {
|
|
1016
|
+
emailsToSend.push({
|
|
1017
|
+
to: userEmail,
|
|
1018
|
+
userName,
|
|
1019
|
+
orderId: order.id,
|
|
1020
|
+
productTitle: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
1021
|
+
quantity: totalQuantity,
|
|
1022
|
+
totalPrice: orderTotal,
|
|
1023
|
+
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
1024
|
+
shippingAddress,
|
|
1025
|
+
paymentMethod: PaymentMethodValues.ONLINE,
|
|
1026
|
+
items: orderItems,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
emitOrderPlacedNotifications({
|
|
1030
|
+
orderId: order.id,
|
|
1031
|
+
buyerUid: uid,
|
|
1032
|
+
buyerName: userName,
|
|
1033
|
+
storeOwnerId,
|
|
1034
|
+
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
1035
|
+
paid: true,
|
|
1036
|
+
whatsappNotifyAddon: whatsappNotifyFee > 0,
|
|
1037
|
+
});
|
|
1038
|
+
return orderTotal;
|
|
1039
|
+
}
|
|
875
1040
|
export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
876
|
-
const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, outOfStockPolicy = OutOfStockPolicyValues.CANCEL_ORDER, } = input;
|
|
1041
|
+
const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, outOfStockPolicy = OutOfStockPolicyValues.CANCEL_ORDER, whatsappNotifyAddon = false, giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, } = input;
|
|
877
1042
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
878
1043
|
const platformFeePercent = siteSettings?.commissions?.platformFeePercent ?? 5;
|
|
879
1044
|
const gstPercent = siteSettings?.commissions?.gstPercent ?? 18;
|
|
1045
|
+
const whatsappNotifyFee = computeWhatsAppNotifyFee(whatsappNotifyAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
1046
|
+
const giftWrapFee = computeGiftWrapFee(giftWrapAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
880
1047
|
const isValid = await verifyPaymentSignatureWithKeys({
|
|
881
1048
|
razorpay_order_id,
|
|
882
1049
|
razorpay_payment_id,
|
|
@@ -914,28 +1081,6 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
914
1081
|
}
|
|
915
1082
|
shippingAddress = formatShippingAddress(resolvedAddressRzp);
|
|
916
1083
|
}
|
|
917
|
-
const db = getAdminDb();
|
|
918
|
-
if (!isDigitalCartRazorpay && addressId) {
|
|
919
|
-
const otpRef = consentOtpRef(db, uid, addressId);
|
|
920
|
-
const otpSnap = await otpRef.get();
|
|
921
|
-
const otpData = otpSnap.exists
|
|
922
|
-
? otpSnap.data()
|
|
923
|
-
: null;
|
|
924
|
-
const isConsentValid = otpData?.verified === true &&
|
|
925
|
-
otpData.expiresAt &&
|
|
926
|
-
(resolveDate(otpData.expiresAt)?.getTime() ?? 0) > Date.now();
|
|
927
|
-
if (!isConsentValid) {
|
|
928
|
-
const reason = !otpData ? "otp_not_verified" : "consent_expired";
|
|
929
|
-
failedCheckoutRepository
|
|
930
|
-
.logPayment(uid, reason, "Consent OTP missing or expired at payment verify time", {
|
|
931
|
-
gatewayOrderId: razorpay_order_id,
|
|
932
|
-
gatewayPaymentId: razorpay_payment_id,
|
|
933
|
-
addressId,
|
|
934
|
-
})
|
|
935
|
-
.catch((err) => { void normalizeError(err); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: err instanceof Error ? err.message : String(err) }); });
|
|
936
|
-
throw new ApiError(403, "Order verification required. Please complete OTP verification and retry.");
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
1084
|
// SB-UNI-5 2026-05-13 — bundle-aware product fetch + validation. Each
|
|
940
1085
|
// cart line gets paired with a "representative" product (first member id
|
|
941
1086
|
// for bundles, productId for regular items). The full bundle-member
|
|
@@ -966,7 +1111,8 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
966
1111
|
// S-SBUNI-RULES 2026-05-13 — sync preflight via rule registry.
|
|
967
1112
|
// Bundle items bypass the cart flow so skip the prize-pool cap.
|
|
968
1113
|
const preflightPairs = productChecks.filter((p) => p.product !== null && p.product !== undefined && !p.item.bundleProductIds?.length);
|
|
969
|
-
|
|
1114
|
+
// This whole action is the Razorpay path — paymentMethod is always "online".
|
|
1115
|
+
runSyncPreflight(preflightPairs, "online");
|
|
970
1116
|
// SB-UNI-O 2026-05-15 — Live-item jurisdiction guard.
|
|
971
1117
|
if (!isDigitalCartRazorpay && resolvedAddressRzp) {
|
|
972
1118
|
assertLiveJurisdiction(cart.items, productByIdPaid, resolvedAddressRzp.state);
|
|
@@ -1051,178 +1197,28 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1051
1197
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s +
|
|
1052
1198
|
g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
1053
1199
|
for (const { items: group, orderType } of orderGroups) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
if (isSellerScoped) {
|
|
1066
|
-
if (coupon.storeId !== firstItem.storeId)
|
|
1067
|
-
continue;
|
|
1068
|
-
if (coupon.applicableItemIds?.length) {
|
|
1069
|
-
const eligibleTotal = group
|
|
1070
|
-
.filter(({ item }) => coupon.applicableItemIds.includes(item.itemId))
|
|
1071
|
-
.reduce((s, { item, product }) => s + unitPriceFor(item, product) * item.quantity, 0);
|
|
1072
|
-
couponGroupDiscount =
|
|
1073
|
-
eligibleTotal > 0
|
|
1074
|
-
? Math.min(roundRupees((eligibleTotal / groupTotal) * coupon.discountAmount), eligibleTotal)
|
|
1075
|
-
: 0;
|
|
1076
|
-
}
|
|
1077
|
-
else {
|
|
1078
|
-
couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
else if (cartSubtotal > 0) {
|
|
1082
|
-
couponGroupDiscount = Math.min(roundRupees((groupTotal / cartSubtotal) * coupon.discountAmount), groupTotal);
|
|
1083
|
-
}
|
|
1084
|
-
if (couponGroupDiscount > 0) {
|
|
1085
|
-
couponDiscount += couponGroupDiscount;
|
|
1086
|
-
appliedDiscounts.push({
|
|
1087
|
-
code: coupon.code,
|
|
1088
|
-
couponId: coupon.couponId,
|
|
1089
|
-
type: "coupon",
|
|
1090
|
-
discountAmount: couponGroupDiscount,
|
|
1091
|
-
scope: coupon.scope,
|
|
1092
|
-
storeId: coupon.storeId,
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
1097
|
-
const rawPlatformFee = roundRupees(groupTotal * (platformFeePercent / 100));
|
|
1098
|
-
const gstOnFee = roundRupees(rawPlatformFee * (gstPercent / 100));
|
|
1099
|
-
const platformFee = rawPlatformFee + gstOnFee;
|
|
1100
|
-
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee;
|
|
1101
|
-
total += orderTotal;
|
|
1102
|
-
// P-8 GST — deliberately NOT wired into this Razorpay-verify path. The
|
|
1103
|
-
// amount-mismatch check above (expectedPaymentAmountRs) compares against
|
|
1104
|
-
// what the buyer already paid via the Razorpay order created earlier in
|
|
1105
|
-
// the flow; adding product GST here without also adding it to that
|
|
1106
|
-
// upstream pre-payment amount calculation would either fail the mismatch
|
|
1107
|
-
// check or silently under/over-charge. Wiring GST through the full
|
|
1108
|
-
// Razorpay create→verify round-trip is separate follow-up work, tracked
|
|
1109
|
-
// alongside P-13 (Razorpay is disabled by default today, so this order
|
|
1110
|
-
// type doesn't currently carry a GST breakdown).
|
|
1111
|
-
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
1112
|
-
const orderItems = group.map(({ item, product }) => {
|
|
1113
|
-
const lt = (product?.listingType ?? "standard");
|
|
1114
|
-
const itemRule = getListingRule(lt);
|
|
1115
|
-
// SB-UNI-5 2026-05-13 — bundle cart-lines surface the locked
|
|
1116
|
-
// bundlePrice (item.price), not the representative member's
|
|
1117
|
-
// product.price. Stock decrement still runs per member elsewhere.
|
|
1118
|
-
const isBundle = Boolean(item.bundleCategorySlug && item.bundleProductIds?.length);
|
|
1119
|
-
const unitPrice = isBundle ? item.price : product.price;
|
|
1120
|
-
const bundleFields = isBundle
|
|
1121
|
-
? {
|
|
1122
|
-
bundleCategorySlug: item.bundleCategorySlug,
|
|
1123
|
-
bundleProductIds: item.bundleProductIds,
|
|
1124
|
-
}
|
|
1125
|
-
: {};
|
|
1126
|
-
const baseLine = {
|
|
1127
|
-
productId: item.productId,
|
|
1128
|
-
productTitle: item.productTitle,
|
|
1129
|
-
quantity: item.quantity,
|
|
1130
|
-
unitPrice,
|
|
1131
|
-
totalPrice: unitPrice * item.quantity,
|
|
1132
|
-
...bundleFields,
|
|
1133
|
-
};
|
|
1134
|
-
return itemRule.decorateOrderItem(baseLine, product);
|
|
1135
|
-
});
|
|
1136
|
-
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
1137
|
-
const imageUrls = [
|
|
1138
|
-
...new Set(group
|
|
1139
|
-
.map(({ product }) => product?.mainImage)
|
|
1140
|
-
.filter((url) => typeof url === "string" && url.length > 0)),
|
|
1141
|
-
];
|
|
1142
|
-
// S-SBUNI-RULES 2026-05-13 — order-doc decoration via rule registry.
|
|
1143
|
-
const lt0Rzp = (group[0].product?.listingType ?? "standard");
|
|
1144
|
-
const groupRuleRzp = getListingRule(lt0Rzp);
|
|
1145
|
-
const extraOrderFields = {
|
|
1146
|
-
...groupRuleRzp.decorateOrderDoc(group[0].item, group[0].product),
|
|
1147
|
-
...(orderType === "prize-draw" && group[0].product
|
|
1148
|
-
? { prizeRevealDeadline: computePrizeRevealDeadline(group[0].product) }
|
|
1149
|
-
: {}),
|
|
1150
|
-
};
|
|
1151
|
-
const order = await unitOfWork.orders.create({
|
|
1152
|
-
productId: firstItem.productId,
|
|
1153
|
-
productTitle: firstItem.productTitle,
|
|
1154
|
-
userId: uid,
|
|
1200
|
+
total += await createRazorpayGroupOrder(group, orderType, {
|
|
1201
|
+
appliedCoupons,
|
|
1202
|
+
cartSubtotal,
|
|
1203
|
+
platformFeePercent,
|
|
1204
|
+
gstPercent,
|
|
1205
|
+
whatsappNotifyFee,
|
|
1206
|
+
giftWrapFee,
|
|
1207
|
+
giftWrapMessage,
|
|
1208
|
+
shipmentProtectionAddon,
|
|
1209
|
+
siteSettings,
|
|
1210
|
+
uid,
|
|
1155
1211
|
userName,
|
|
1156
1212
|
userEmail,
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
1161
|
-
storeId: firstItem.storeId || undefined,
|
|
1162
|
-
storeName: firstItem.storeName || undefined,
|
|
1163
|
-
items: orderItems,
|
|
1164
|
-
orderType,
|
|
1165
|
-
offerId: firstItem.offerId ?? undefined,
|
|
1166
|
-
status: OrderStatusValues.CONFIRMED,
|
|
1167
|
-
paymentStatus: PaymentStatusValues.PAID,
|
|
1168
|
-
paymentMethod: PaymentMethodValues.ONLINE,
|
|
1169
|
-
paymentId: razorpay_payment_id,
|
|
1170
|
-
paymentRecord: {
|
|
1171
|
-
method: "razorpay",
|
|
1172
|
-
transactionId: razorpay_payment_id,
|
|
1173
|
-
amount: orderTotal,
|
|
1174
|
-
paidAt: new Date(),
|
|
1175
|
-
verifiedBy: "razorpay-webhook",
|
|
1176
|
-
verificationMethod: "webhook",
|
|
1177
|
-
gatewayRef: {
|
|
1178
|
-
orderId: razorpay_order_id,
|
|
1179
|
-
paymentId: razorpay_payment_id,
|
|
1180
|
-
signature: razorpay_signature,
|
|
1181
|
-
},
|
|
1182
|
-
},
|
|
1213
|
+
razorpay_order_id,
|
|
1214
|
+
razorpay_payment_id,
|
|
1215
|
+
razorpay_signature,
|
|
1183
1216
|
shippingAddress,
|
|
1184
1217
|
notes,
|
|
1185
|
-
platformFee,
|
|
1186
|
-
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
1187
|
-
couponCode: appliedDiscounts[0]?.code,
|
|
1188
|
-
couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
|
|
1189
|
-
appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
|
|
1190
|
-
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
|
1191
1218
|
outOfStockPolicy,
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
orderIds.push(order.id);
|
|
1196
|
-
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
1197
|
-
// order is already persisted so a claim failure is logged, not thrown).
|
|
1198
|
-
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
1199
|
-
claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid, {
|
|
1200
|
-
userEmail: userEmail || undefined,
|
|
1201
|
-
userName,
|
|
1202
|
-
productTitle: firstItem.productTitle,
|
|
1203
|
-
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
1204
|
-
}
|
|
1205
|
-
if (userEmail) {
|
|
1206
|
-
emailsToSend.push({
|
|
1207
|
-
to: userEmail,
|
|
1208
|
-
userName,
|
|
1209
|
-
orderId: order.id,
|
|
1210
|
-
productTitle: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
1211
|
-
quantity: totalQuantity,
|
|
1212
|
-
totalPrice: orderTotal,
|
|
1213
|
-
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
1214
|
-
shippingAddress,
|
|
1215
|
-
paymentMethod: PaymentMethodValues.ONLINE,
|
|
1216
|
-
items: orderItems,
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
emitOrderPlacedNotifications({
|
|
1220
|
-
orderId: order.id,
|
|
1221
|
-
buyerUid: uid,
|
|
1222
|
-
buyerName: userName,
|
|
1223
|
-
storeOwnerId,
|
|
1224
|
-
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
1225
|
-
paid: true,
|
|
1219
|
+
unavailablePaid,
|
|
1220
|
+
orderIds,
|
|
1221
|
+
emailsToSend,
|
|
1226
1222
|
});
|
|
1227
1223
|
}
|
|
1228
1224
|
// SB-UNI-5 2026-05-13 — bundle-aware batch decrement. We iterate the
|
|
@@ -1247,10 +1243,6 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1247
1243
|
selectedItemIds: null,
|
|
1248
1244
|
});
|
|
1249
1245
|
});
|
|
1250
|
-
if (!isDigitalCartRazorpay && addressId) {
|
|
1251
|
-
const otpRefForDelete = consentOtpRef(db, uid, addressId);
|
|
1252
|
-
safeFireAndForget(otpRefForDelete.delete(), "checkout: delete OTP consent ref after Razorpay payment");
|
|
1253
|
-
}
|
|
1254
1246
|
// outOfStockPolicy: "skip_items" — auto-refund the dropped items' value.
|
|
1255
1247
|
// Razorpay already captured payment for the FULL cart before this stock
|
|
1256
1248
|
// check ran, so anything not placed as an order must be given back.
|