@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.
Files changed (276) hide show
  1. package/dist/_internal/server/features/auth/actions.d.ts +0 -1
  2. package/dist/_internal/server/features/auth/actions.js +0 -1
  3. package/dist/_internal/server/features/bundles/data.d.ts +22 -4
  4. package/dist/_internal/server/features/bundles/data.js +41 -4
  5. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  6. package/dist/_internal/server/features/bundles/index.js +1 -1
  7. package/dist/_internal/server/features/checkout/actions.d.ts +15 -0
  8. package/dist/_internal/server/features/checkout/actions.js +249 -257
  9. package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
  10. package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
  11. package/dist/_internal/server/features/orders/adapters.js +1 -14
  12. package/dist/_internal/server/features/products/actions.js +8 -3
  13. package/dist/_internal/server/features/products/service.d.ts +15 -0
  14. package/dist/_internal/server/features/products/service.js +41 -0
  15. package/dist/_internal/server/functions/firestore.d.ts +3 -1
  16. package/dist/_internal/server/functions/firestore.js +17 -1
  17. package/dist/_internal/server/functions/scheduled.d.ts +3 -5
  18. package/dist/_internal/server/functions/scheduled.js +14 -30
  19. package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
  20. package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
  21. package/dist/_internal/server/jobs/core/index.d.ts +4 -4
  22. package/dist/_internal/server/jobs/core/index.js +4 -4
  23. package/dist/_internal/server/jobs/core/jobRunners.js +10 -0
  24. package/dist/_internal/server/jobs/core/newsletterExport.d.ts +15 -0
  25. package/dist/_internal/server/jobs/core/newsletterExport.js +43 -0
  26. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
  27. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
  28. package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
  29. package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
  30. package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
  31. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +3 -2
  32. package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
  33. package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
  34. package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
  35. package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
  36. package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
  37. package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
  38. package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
  39. package/dist/_internal/server/jobs/core/revenueRollup.d.ts +13 -0
  40. package/dist/_internal/server/jobs/core/revenueRollup.js +22 -0
  41. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
  42. package/dist/_internal/server/jobs/core/whatsappNotify.js +82 -0
  43. package/dist/_internal/server/jobs/handlers/index.d.ts +4 -4
  44. package/dist/_internal/server/jobs/handlers/index.js +9 -6
  45. package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
  46. package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
  47. package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
  48. package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
  49. package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
  50. package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
  51. package/dist/_internal/server/jobs/handlers/revenueRollup.d.ts +2 -0
  52. package/dist/_internal/server/jobs/handlers/revenueRollup.js +2 -0
  53. package/dist/_internal/shared/actions/action-registry.js +40 -10
  54. package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
  55. package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
  56. package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
  57. package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
  58. package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
  59. package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
  60. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  61. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  62. package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
  63. package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
  64. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
  65. package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
  66. package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
  67. package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
  68. package/dist/_internal/shared/features/checkout/config.d.ts +7 -0
  69. package/dist/_internal/shared/features/checkout/config.js +7 -0
  70. package/dist/_internal/shared/fees/calculator.d.ts +26 -0
  71. package/dist/_internal/shared/fees/calculator.js +25 -0
  72. package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
  73. package/dist/constants/api-endpoints.d.ts +6 -9
  74. package/dist/constants/api-endpoints.js +5 -5
  75. package/dist/constants/field-names.d.ts +17 -1
  76. package/dist/constants/field-names.js +17 -1
  77. package/dist/features/about/components/FeesView.js +18 -0
  78. package/dist/features/account/hooks/useNotifications.js +5 -0
  79. package/dist/features/admin/actions/admin-actions.js +6 -0
  80. package/dist/features/admin/actions/notification-actions.d.ts +4 -1
  81. package/dist/features/admin/actions/notification-actions.js +57 -12
  82. package/dist/features/admin/components/AdminAdsView.js +1 -1
  83. package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
  84. package/dist/features/admin/components/AdminNewsletterView.js +42 -11
  85. package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
  86. package/dist/features/admin/components/AdminSiteSettingsView.js +115 -11
  87. package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
  88. package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
  89. package/dist/features/admin/hooks/useAdminListing.js +4 -2
  90. package/dist/features/admin/repository/analytics-rollup.repository.d.ts +25 -0
  91. package/dist/features/admin/repository/analytics-rollup.repository.js +31 -0
  92. package/dist/features/admin/repository/site-settings.repository.js +8 -1
  93. package/dist/features/admin/schemas/firestore.d.ts +54 -5
  94. package/dist/features/admin/schemas/firestore.js +63 -26
  95. package/dist/features/auctions/actions/bid-actions.js +3 -1
  96. package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
  97. package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
  98. package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
  99. package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
  100. package/dist/features/auctions/components/CollapsibleBidHistory.js +21 -3
  101. package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
  102. package/dist/features/auctions/components/LiveBidPrice.js +17 -0
  103. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
  104. package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
  105. package/dist/features/auctions/hooks/useBids.d.ts +33 -0
  106. package/dist/features/auctions/hooks/useBids.js +32 -0
  107. package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
  108. package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
  109. package/dist/features/auth/checkout-value-otp.d.ts +7 -10
  110. package/dist/features/auth/checkout-value-otp.js +14 -10
  111. package/dist/features/auth/server.d.ts +0 -1
  112. package/dist/features/auth/server.js +0 -1
  113. package/dist/features/blog/api/[slug]/route.d.ts +5 -0
  114. package/dist/features/blog/api/[slug]/route.js +12 -3
  115. package/dist/features/blog/components/BlogPostView.d.ts +2 -0
  116. package/dist/features/blog/components/BlogPostView.js +5 -4
  117. package/dist/features/blog/hooks/useBlog.d.ts +2 -0
  118. package/dist/features/blog/hooks/useBlog.js +2 -0
  119. package/dist/features/blog/repository/blog.repository.d.ts +7 -0
  120. package/dist/features/blog/repository/blog.repository.js +44 -0
  121. package/dist/features/blog/schemas/firestore.d.ts +3 -0
  122. package/dist/features/blog/schemas/firestore.js +1 -0
  123. package/dist/features/blog/types/index.d.ts +2 -0
  124. package/dist/features/cart/components/index.d.ts +0 -2
  125. package/dist/features/cart/components/index.js +0 -1
  126. package/dist/features/cart/hooks/useCartCount.js +22 -5
  127. package/dist/features/cart/hooks/useGuestCart.js +11 -1
  128. package/dist/features/cart/utils/guest-cart.d.ts +4 -0
  129. package/dist/features/cart/utils/guest-cart.js +10 -0
  130. package/dist/features/cart/utils/pending-ops.d.ts +17 -0
  131. package/dist/features/cart/utils/pending-ops.js +46 -5
  132. package/dist/features/categories/components/BrandDetailPageView.js +6 -2
  133. package/dist/features/categories/components/BundleDetailView.js +4 -2
  134. package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
  135. package/dist/features/categories/schemas/firestore.d.ts +7 -0
  136. package/dist/features/checkout/actions/index.d.ts +0 -1
  137. package/dist/features/checkout/actions/index.js +0 -1
  138. package/dist/features/checkout/schemas/firestore.d.ts +2 -2
  139. package/dist/features/events/actions/event-actions.d.ts +2 -0
  140. package/dist/features/events/actions/event-actions.js +5 -0
  141. package/dist/features/events/components/EventCard.js +1 -2
  142. package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
  143. package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
  144. package/dist/features/events/components/index.d.ts +2 -0
  145. package/dist/features/events/components/index.js +1 -0
  146. package/dist/features/events/repository/events.repository.d.ts +6 -0
  147. package/dist/features/events/repository/events.repository.js +21 -0
  148. package/dist/features/history/repository/user-history.repository.d.ts +1 -1
  149. package/dist/features/history/utils/guest-history.d.ts +1 -1
  150. package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
  151. package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
  152. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  153. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  154. package/dist/features/layout/AppLayoutShell.js +1 -1
  155. package/dist/features/layout/BackToTop.d.ts +9 -5
  156. package/dist/features/layout/BackToTop.js +17 -21
  157. package/dist/features/layout/NavbarLayout.js +1 -1
  158. package/dist/features/layout/TitleBarLayout.js +2 -2
  159. package/dist/features/media/MediaVideo.d.ts +7 -0
  160. package/dist/features/media/MediaVideo.js +36 -5
  161. package/dist/features/orders/actions/refund-actions.js +1 -0
  162. package/dist/features/orders/components/OrdersList.js +1 -5
  163. package/dist/features/orders/schemas/firestore.d.ts +16 -8
  164. package/dist/features/orders/schemas/index.d.ts +3 -32
  165. package/dist/features/orders/schemas/index.js +1 -4
  166. package/dist/features/orders/types/index.d.ts +6 -9
  167. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  168. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
  169. package/dist/features/products/actions/product-actions.js +13 -10
  170. package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
  171. package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
  172. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  173. package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
  174. package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
  175. package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
  176. package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
  177. package/dist/features/products/components/PrizeRevealModal.js +18 -147
  178. package/dist/features/products/components/ProductDetailPageView.js +72 -30
  179. package/dist/features/products/components/ProductForm.js +7 -14
  180. package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
  181. package/dist/features/products/components/ProductGalleryClient.js +29 -10
  182. package/dist/features/products/components/ProductsIndexListing.js +2 -1
  183. package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
  184. package/dist/features/products/components/ShowGroupSection.js +2 -2
  185. package/dist/features/products/components/SublistingCarouselSection.js +1 -1
  186. package/dist/features/products/components/index.d.ts +4 -2
  187. package/dist/features/products/components/index.js +1 -0
  188. package/dist/features/products/constants/action-defs.d.ts +28 -4
  189. package/dist/features/products/constants/action-defs.js +76 -3
  190. package/dist/features/products/repository/products.repository.d.ts +6 -0
  191. package/dist/features/products/repository/products.repository.js +20 -1
  192. package/dist/features/products/schemas/firestore.d.ts +26 -2
  193. package/dist/features/products/schemas/index.d.ts +11 -3
  194. package/dist/features/products/schemas/index.js +10 -2
  195. package/dist/features/products/types/index.d.ts +2 -1
  196. package/dist/features/reviews/actions/review-actions.js +1 -0
  197. package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
  198. package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
  199. package/dist/features/reviews/components/ReviewsList.js +2 -1
  200. package/dist/features/reviews/schemas/firestore.d.ts +3 -0
  201. package/dist/features/reviews/schemas/index.d.ts +6 -0
  202. package/dist/features/reviews/schemas/index.js +2 -0
  203. package/dist/features/reviews/types/index.d.ts +3 -0
  204. package/dist/features/scams/actions/scam-actions.d.ts +3 -0
  205. package/dist/features/scams/actions/scam-actions.js +3 -2
  206. package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
  207. package/dist/features/scams/components/ScamProfileView.js +2 -2
  208. package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
  209. package/dist/features/scams/repository/scammer.repository.js +24 -0
  210. package/dist/features/seller/actions/seller-actions.js +8 -0
  211. package/dist/features/seller/components/SellerOrdersView.js +1 -1
  212. package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
  213. package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
  214. package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
  215. package/dist/features/stores/components/StoreNavTabs.js +21 -9
  216. package/dist/features/tester/actions/index.d.ts +1 -0
  217. package/dist/features/tester/actions/index.js +1 -0
  218. package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
  219. package/dist/features/tester/actions/leaderboard-actions.js +5 -0
  220. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
  221. package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
  222. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
  223. package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
  224. package/dist/features/tester/components/index.d.ts +2 -0
  225. package/dist/features/tester/components/index.js +1 -0
  226. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
  227. package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
  228. package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
  229. package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
  230. package/dist/features/tester/schemas/firestore.d.ts +22 -1
  231. package/dist/features/tester/schemas/firestore.js +8 -0
  232. package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
  233. package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
  234. package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
  235. package/dist/features/tester/seed-data/products-tester-seed-data.js +152 -8
  236. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +249 -14
  237. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +10 -1
  238. package/dist/features/whatsapp-bot/helpers/whatsapp.js +47 -0
  239. package/dist/features/whatsapp-bot/types/index.d.ts +19 -0
  240. package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
  241. package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
  242. package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
  243. package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
  244. package/dist/index.d.ts +23 -24
  245. package/dist/index.js +29 -60
  246. package/dist/jobs.d.ts +1 -1
  247. package/dist/jobs.js +2 -2
  248. package/dist/next/routing/route-map.d.ts +4 -0
  249. package/dist/next/routing/route-map.js +2 -0
  250. package/dist/repositories/index.d.ts +2 -0
  251. package/dist/repositories/index.js +1 -0
  252. package/dist/schemas/registry.d.ts +6 -26
  253. package/dist/seed/bids-seed-data.js +14 -2
  254. package/dist/seed/blog-posts-seed-data.js +2 -1
  255. package/dist/seed/categories-seed-data.js +11 -2
  256. package/dist/seed/events-seed-data.js +11 -11
  257. package/dist/seed/faq-seed-data.js +5 -1
  258. package/dist/seed/products-auctions-seed-data.js +2 -2
  259. package/dist/seed/products-preorders-seed-data.d.ts +2 -1
  260. package/dist/seed/products-prize-draws-seed-data.js +56 -10
  261. package/dist/seed/products-standard-seed-data.js +82 -3
  262. package/dist/seed/scammers-seed-data.js +75 -2
  263. package/dist/seed/site-settings-seed-data.js +12 -2
  264. package/dist/server-entry.d.ts +0 -1
  265. package/dist/server-entry.js +0 -1
  266. package/dist/server.d.ts +6 -22
  267. package/dist/server.js +12 -59
  268. package/dist/styles.css +1 -1
  269. package/dist/tailwind-utilities.css +1 -1
  270. package/dist/ui/components/BaseListingCard.js +1 -1
  271. package/dist/ui/components/ImageLightbox.d.ts +9 -0
  272. package/dist/ui/components/ImageLightbox.js +6 -3
  273. package/dist/ui/components/ListingToolbar.d.ts +18 -1
  274. package/dist/ui/components/ListingToolbar.js +5 -2
  275. package/dist/ui/components/StickyToolbar.js +13 -8
  276. 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, notificationRepository, claimedCouponsRepository, addressesRepository, } from "../../../../repositories";
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 { resolveDate } from "../../../../utils";
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, computePrizeRevealDeadline, } from "./prize-bundle-gates";
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 = notificationRepository
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
- .catch((err) => serverLogger.warn("Failed to create buyer order_placed notification", {
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
- ? notificationRepository
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, emailOtpUsed };
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, reason, err instanceof Error ? err.message : String(err), {
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, emailOtpUsed } = stockResult;
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
- runSyncPreflight(preflightPairs);
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
- const firstItem = group[0].item;
1055
- const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
1056
- // Reuses the same store/seller lookup as the COD/UPI path above instead
1057
- // of re-implementing it inline — was two sequential findById calls per
1058
- // seller group here, duplicated from resolveShippingCost.
1059
- const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId);
1060
- let couponDiscount = 0;
1061
- const appliedDiscounts = [];
1062
- for (const coupon of appliedCoupons) {
1063
- let couponGroupDiscount = 0;
1064
- const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
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
- quantity: totalQuantity,
1158
- unitPrice: group[0].product.price,
1159
- totalPrice: orderTotal,
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
- droppedItems: unavailablePaid.length > 0 ? unavailablePaid : undefined,
1193
- ...extraOrderFields,
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.