@mohasinac/appkit 4.62.0 → 4.63.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/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +129 -9
- package/dist/_internal/server/features/checkout/actions.js +850 -79
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +54 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +264 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +2 -2
- package/dist/_internal/server/features/site-settings/adapters.js +29 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +65 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +95 -2
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +11 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +65 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +25 -2
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +296 -0
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +174 -0
- package/dist/_internal/shared/checkout/order-math.js +159 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -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/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +75 -0
- package/dist/_internal/shared/checkout/split.js +120 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +93 -0
- package/dist/_internal/shared/content/published-values.js +206 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +139 -0
- package/dist/_internal/shared/features/admin/integrations.js +263 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +29 -0
- package/dist/_internal/shared/features/points/accepts.js +49 -0
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.js +43 -0
- package/dist/constants/api-endpoints.d.ts +114 -0
- package/dist/constants/api-endpoints.js +55 -0
- package/dist/constants/field-names.d.ts +42 -0
- package/dist/constants/field-names.js +52 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOrdersView.js +47 -35
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +33 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +81 -3
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +86 -5
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +62 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/ads/schemas/booking-write.d.ts +120 -0
- package/dist/features/ads/schemas/booking-write.js +75 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +140 -0
- package/dist/features/audit-log/schemas/firestore.js +89 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +25 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +29 -6
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +33 -0
- package/dist/features/cart/repository/cart.repository.js +47 -0
- package/dist/features/cart/schemas/firestore.d.ts +116 -0
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +62 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +108 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +62 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +119 -0
- package/dist/features/orders/components/RefundRequestView.js +19 -1
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +85 -1
- package/dist/features/orders/repository/orders.repository.js +205 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +398 -1
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +56 -8
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +47 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +37 -3
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +221 -27
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +82 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +145 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +80 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +112 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +8 -8
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +33 -1
- package/dist/next/routing/route-map.d.ts +112 -0
- package/dist/next/routing/route-map.js +63 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +69 -34
- package/dist/schemas/sieve.d.ts +2 -2
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +36 -1
- package/dist/server.js +183 -1
- package/dist/styles.css +412 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +83 -0
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +8 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +83 -79
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1834 -1822
|
@@ -16,7 +16,18 @@ import { roundShippingUp, } from "../../../shared/shipping/quote.js";
|
|
|
16
16
|
import { unitOfWork, adminNotificationsRepository, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, productRepository, } from "../../../../repositories/index.js";
|
|
17
17
|
import { isIntraStateSupply } from "../../../shared/fees/calculator.js";
|
|
18
18
|
import { resolveStateKey, INDIA_STATES } from "../../../shared/fees/india-states.js";
|
|
19
|
-
import { computePreOrderDepositAmount, lineTotalFor, sumGroupGst, unitPriceFor, } from "../../../shared/checkout/order-math.js";
|
|
19
|
+
import { allocatePointsAcrossGroups, pointsBaseForGroup, computePreOrderDepositAmount, lineTotalFor, resolvePointsTender, sumGroupGst, unitPriceFor, } from "../../../shared/checkout/order-math.js";
|
|
20
|
+
/*
|
|
21
|
+
* D46/D47 — points as a checkout TENDER.
|
|
22
|
+
*
|
|
23
|
+
* `storeAcceptsPoints` resolves decision 22's per-store opt-in (and the
|
|
24
|
+
* platform store's always-accepts, which is decided in code); the repository
|
|
25
|
+
* supplies the balance and owns the ledger write. The ARITHMETIC lives in
|
|
26
|
+
* `resolvePointsTender` above, beside every other money rule, so the tender
|
|
27
|
+
* cannot become a sixth disagreeing total (D37/D38/D40).
|
|
28
|
+
*/
|
|
29
|
+
import { storeAcceptsPoints } from "../../../shared/features/points/accepts.js";
|
|
30
|
+
import { pointsRepository } from "../../../../features/points/repository/points.repository.js";
|
|
20
31
|
import { COD_MAX_ORDER_VALUE_FALLBACK } from "../../../../features/admin/schemas/firestore.js";
|
|
21
32
|
import { CHECKOUT_ERROR_CODES } from "../../../../errors/error-codes.js";
|
|
22
33
|
import { failedCheckoutRepository } from "../../../../features/checkout/repository/failed-checkout.repository.js";
|
|
@@ -31,16 +42,21 @@ import { DEFAULT_STORE_SHIPPING_POLICY, quoteShipping, toStoreShippingPolicy, }
|
|
|
31
42
|
*/
|
|
32
43
|
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
33
44
|
import { computeProtectionFees } from "../../../shared/features/shipping/protections.js";
|
|
45
|
+
/* S5a — one payer, N destinations. Pure: the rates come in snapshotted, and
|
|
46
|
+
* an UNPRICED arrangement is refused rather than quoted at ₹0 (D44's lesson). */
|
|
47
|
+
import { countDestinations, quoteSharedPurchase, } from "../../../shared/features/shipping/shared-purchase.js";
|
|
34
48
|
import { resolveCouponCategorySlugs } from "../../../../features/promotions/actions/coupon-actions.js";
|
|
35
49
|
import { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines.js";
|
|
36
50
|
import { activeLane, defaultCheckoutItems, resolveCheckoutItems, laneOf, } from "../../../shared/checkout/lanes.js";
|
|
37
51
|
import { computeCodHandlingFee, computeGiftWrapFee, computeShipmentProtectionFee, computeSaleProtectionFee, computeCheckoutFees, allocateCheckoutFees, } from "../../../shared/fees/calculator.js";
|
|
38
52
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase/index.js";
|
|
53
|
+
import { snapshotDeliveryAddress, } from "../../../shared/features/orders/delivery-snapshot.js";
|
|
39
54
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore.js";
|
|
40
55
|
import { CART_COLLECTION } from "../../../../features/cart/schemas/index.js";
|
|
41
56
|
// S-SBUNI-RULES 2026-05-13 — bundle cart-line stock fan-out helpers.
|
|
42
57
|
import { getCartItemMemberIds, getExpandedDecrements, bucketCartItemsByStock, isMultiMemberLine, } from "./bundle-expansion.js";
|
|
43
58
|
import { expandCartLineToOrderRows } from "./expand-order-items.js";
|
|
59
|
+
import { freezePartsOnItems } from "./freeze-parts.js";
|
|
44
60
|
import { OrderStatusValues, PaymentStatusValues, PaymentMethodValues, OutOfStockPolicyValues, } from "../../../../features/orders/schemas/index.js";
|
|
45
61
|
import { getDefaultCurrency } from "../../../../core/index.js";
|
|
46
62
|
import { getProviders } from "../../../../contracts/index.js";
|
|
@@ -53,10 +69,11 @@ import { verifyPaymentSignature as verifyRazorpayPaymentSignature } from "../../
|
|
|
53
69
|
import { resolveKeys } from "../../../../core/integration-keys.js";
|
|
54
70
|
import { paiseToRupees } from "../../../../core/money.js";
|
|
55
71
|
import { CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_DEFAULT_EMI_SETTINGS } from "../../../shared/features/checkout/config.js";
|
|
56
|
-
import {
|
|
72
|
+
import { emiTenderFor, resolveEmiChoice } from "../../../shared/features/emi/choice.js";
|
|
73
|
+
import { amountDueNow, emiOrderFields, groupEmiGate, groupEmiQuote, groupEmiSchedule, } from "../../../shared/features/emi/group.js";
|
|
57
74
|
import { formatShippingAddress } from "./data.js";
|
|
58
75
|
import { enforceMaxPerUserForCart } from "./prize-bundle-gates.js";
|
|
59
|
-
import { getListingRule, runSyncPreflight, assertPaymentMethodAllowed, } from "../../../shared/checkout/rules/index.js";
|
|
76
|
+
import { getListingRule, runSyncPreflight, assertPaymentMethodAllowed, assertEmiOptInAllowed, } from "../../../shared/checkout/rules/index.js";
|
|
60
77
|
// The `GroupCouponDiscountResult` type is deliberately NOT imported: every
|
|
61
78
|
// call site here destructures the result inline, so naming the type would be
|
|
62
79
|
// an unused import.
|
|
@@ -68,7 +85,7 @@ import { sendNotification } from "../../../../features/admin/actions/notificatio
|
|
|
68
85
|
import { resolveDisplayedUpiId } from "./upi-resolution.js";
|
|
69
86
|
import { resolveSupplierIdentity } from "./supplier-identity.js";
|
|
70
87
|
import { PAYMENT_WINDOW_MS } from "../../../../features/orders/constants/payment-window.js";
|
|
71
|
-
import {
|
|
88
|
+
import { assertCheckoutValueOtp } from "./value-otp-gate.js";
|
|
72
89
|
const AUDIT_LOG_FAIL_MSG = "checkout: audit log failed (non-critical)";
|
|
73
90
|
/**
|
|
74
91
|
* How long a `gatewayOrderClaims` holder owns the right to place an order
|
|
@@ -262,6 +279,10 @@ opts) {
|
|
|
262
279
|
shippingPolicy: DEFAULT_STORE_SHIPPING_POLICY,
|
|
263
280
|
shippingUnavailable: false,
|
|
264
281
|
storeProtections: { shipping: false, sale: false },
|
|
282
|
+
// No store, so no seller to absorb a redemption.
|
|
283
|
+
acceptsPoints: false,
|
|
284
|
+
// No store, so nobody to pack N parcels.
|
|
285
|
+
sharedPurchaseRates: { allowSharedPurchase: false },
|
|
265
286
|
// No store, so no seller registration — the caller falls back to the
|
|
266
287
|
// platform's via resolveSupplierIdentity.
|
|
267
288
|
storeTaxIdentity: undefined,
|
|
@@ -381,6 +402,13 @@ opts) {
|
|
|
381
402
|
shipping: store?.supportsShippingProtection === true,
|
|
382
403
|
sale: store?.supportsSaleProtection === true,
|
|
383
404
|
},
|
|
405
|
+
acceptsPoints: storeAcceptsPoints(store, storeId),
|
|
406
|
+
// S5a — raw, so the caller can tell "not configured" from ₹0.
|
|
407
|
+
sharedPurchaseRates: {
|
|
408
|
+
allowSharedPurchase: store?.allowSharedPurchase === true,
|
|
409
|
+
sharedPurchaseHandlingFee: store?.sharedPurchaseHandlingFee,
|
|
410
|
+
sharedPurchaseExtraShippingPerDestination: store?.sharedPurchaseExtraShippingPerDestination,
|
|
411
|
+
},
|
|
384
412
|
/*
|
|
385
413
|
* The seller's own tax registration, returned from the store read this
|
|
386
414
|
* function ALREADY does.
|
|
@@ -594,6 +622,112 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
594
622
|
return;
|
|
595
623
|
Promise.all(emailsToSend.map((e) => sendOrderConfirmationEmail(e))).catch((err) => serverLogger.error("Order confirmation email error:", err));
|
|
596
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* The buyer's points balance, or **0** if it cannot be read.
|
|
627
|
+
*
|
|
628
|
+
* 🛑 Failing OPEN to zero is the safe direction and the only safe direction: a
|
|
629
|
+
* balance we could not read becomes "no discount applied", so the buyer is
|
|
630
|
+
* charged the full amount and their points are untouched. The inverse — guessing
|
|
631
|
+
* a balance — would reduce the charge against points that may not exist, which
|
|
632
|
+
* is the shape of the deleted D46 block (it debited points and reduced nothing;
|
|
633
|
+
* this is the same mistake with the signs swapped).
|
|
634
|
+
*
|
|
635
|
+
* A buyer who opted in and sees no discount can retry. There is no recoverable
|
|
636
|
+
* outcome on the other side.
|
|
637
|
+
*/
|
|
638
|
+
async function safePointsBalance(userId) {
|
|
639
|
+
try {
|
|
640
|
+
return await pointsRepository.getBalance(userId);
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
const normalized = normalizeError(err);
|
|
644
|
+
serverLogger.warn("[checkout] points balance unreadable — charging full amount", {
|
|
645
|
+
userId,
|
|
646
|
+
error: normalized.message,
|
|
647
|
+
});
|
|
648
|
+
return 0;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Debit the points an order was discounted by — the second half of the tender.
|
|
653
|
+
*
|
|
654
|
+
* 🛑 ONE implementation, called from BOTH order-creation paths. They are
|
|
655
|
+
* ~200-line twins that have already drifted once (add-on fees, Root Cause
|
|
656
|
+
* #65), and a money rule reachable by copy-paste gets copied (Root Cause #75).
|
|
657
|
+
*
|
|
658
|
+
* Runs AFTER the order exists and BEFORE the gateway is asked for anything.
|
|
659
|
+
* That ordering is the whole D46 fix: the deleted block ran after capture, so a
|
|
660
|
+
* redemption that succeeded took points from a buyer who had just paid full
|
|
661
|
+
* price while its own comment claimed "the balance intact".
|
|
662
|
+
*
|
|
663
|
+
* 🛑 A REFUSED REDEEM REVERTS THE DISCOUNT; it is never swallowed. `redeem`
|
|
664
|
+
* re-reads the balance inside its own transaction and refuses rather than going
|
|
665
|
+
* negative — so a refusal means the budget was wrong and the order is recording
|
|
666
|
+
* a discount nobody funded. Reverting is the only honest response: the buyer
|
|
667
|
+
* pays full price, which they can see and query, where an unfunded discount is
|
|
668
|
+
* a silent under-collection on every order that hits it.
|
|
669
|
+
*
|
|
670
|
+
* `orderId` is the natural idempotency key — this runs once per created order,
|
|
671
|
+
* and an order is created once.
|
|
672
|
+
*/
|
|
673
|
+
async function settlePointsTender(input) {
|
|
674
|
+
if (input.pointsApplied <= 0)
|
|
675
|
+
return;
|
|
676
|
+
try {
|
|
677
|
+
await pointsRepository.redeem({
|
|
678
|
+
userId: input.userId,
|
|
679
|
+
points: input.pointsApplied,
|
|
680
|
+
orderId: input.orderId,
|
|
681
|
+
description: `Points applied to order ${input.orderId}`,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
const normalized = normalizeError(err);
|
|
686
|
+
serverLogger.error("[checkout] points redemption refused — reverting the discount", {
|
|
687
|
+
orderId: input.orderId,
|
|
688
|
+
userId: input.userId,
|
|
689
|
+
pointsApplied: input.pointsApplied,
|
|
690
|
+
error: normalized.message,
|
|
691
|
+
});
|
|
692
|
+
// Hand the budget back, so a later group in the same checkout is not
|
|
693
|
+
// charged for a spend that did not happen.
|
|
694
|
+
input.pointsBudget.remaining += input.pointsApplied;
|
|
695
|
+
await unitOfWork.orders
|
|
696
|
+
.update(input.orderId, { pointsApplied: undefined, amountDue: undefined })
|
|
697
|
+
.catch((e) => {
|
|
698
|
+
/*
|
|
699
|
+
* 🛑 The one genuinely bad outcome, so it is reported rather than
|
|
700
|
+
* logged: the order still says `amountDue`, the points were never
|
|
701
|
+
* taken, and the gateway is about to be asked for the discounted
|
|
702
|
+
* figure. A case names the amount and the order; `safeRead`'s silent
|
|
703
|
+
* degrade would be wrong here because nothing downstream can detect it.
|
|
704
|
+
*/
|
|
705
|
+
const inner = normalizeError(e);
|
|
706
|
+
serverLogger.error("[checkout] points revert FAILED — order under-collects", {
|
|
707
|
+
orderId: input.orderId,
|
|
708
|
+
amountUnfunded: input.pointsApplied,
|
|
709
|
+
error: inner.message,
|
|
710
|
+
});
|
|
711
|
+
void paymentCasesRepository()
|
|
712
|
+
.openOrObserve({
|
|
713
|
+
kind: "amount_mismatch",
|
|
714
|
+
// The problem is this order's unfunded discount, so the subject is
|
|
715
|
+
// the order — one case however many times it is observed.
|
|
716
|
+
subjectId: input.orderId,
|
|
717
|
+
severity: "error",
|
|
718
|
+
entityType: "order",
|
|
719
|
+
entityId: input.orderId,
|
|
720
|
+
orderIds: [input.orderId],
|
|
721
|
+
title: "Order carries a points discount that was never funded",
|
|
722
|
+
detail: `Redemption of ${input.pointsApplied} points was refused and the ` +
|
|
723
|
+
`revert also failed, so this order will be captured at its ` +
|
|
724
|
+
`discounted amountDue with no matching ledger debit.`,
|
|
725
|
+
amountAtRisk: input.pointsApplied,
|
|
726
|
+
})
|
|
727
|
+
.catch(() => undefined);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
597
731
|
/**
|
|
598
732
|
* Places one order for one seller-group of the checkout cart — fee math
|
|
599
733
|
* (COD deposit/handling, EMI schedule), coupon apportionment, order-doc
|
|
@@ -602,7 +736,7 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
602
736
|
* so the caller can accumulate the checkout-wide `total`.
|
|
603
737
|
*/
|
|
604
738
|
async function createOrderForGroup(group, orderType, ctx) {
|
|
605
|
-
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, storeAddons, platformFeeByStore, selectedShippingQuote, } = ctx;
|
|
739
|
+
const { paymentMethod, emiOptIn, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, deliverySnapshot, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, storeAddons, platformFeeByStore, selectedShippingQuote, useAllPoints, pointsBudget, } = ctx;
|
|
606
740
|
const firstItem = group[0].item;
|
|
607
741
|
const firstProduct = group[0].product;
|
|
608
742
|
// Add-ons are this store's choice, not the cart's — see OrderGroupContext.
|
|
@@ -636,11 +770,24 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
636
770
|
// A multi-member line expands into one row PER MEMBER sharing a groupSlug, so
|
|
637
771
|
// each row carries its own HSN/GST and can be cancelled independently; the
|
|
638
772
|
// receipt still collapses them under one header.
|
|
639
|
-
const
|
|
773
|
+
const baseOrderItems = group.flatMap(({ item, product }) => {
|
|
640
774
|
const lt = (product.listingType ?? "standard");
|
|
641
775
|
const itemRule = getListingRule(lt);
|
|
642
776
|
return expandCartLineToOrderRows(item, product, ctx.productById).map((baseLine) => itemRule.decorateOrderItem(baseLine, product));
|
|
643
777
|
});
|
|
778
|
+
/*
|
|
779
|
+
* Part 14 — freeze the parts a composed listing was sold as containing.
|
|
780
|
+
*
|
|
781
|
+
* A separate pass because composition lives in `modelCompositions` keyed by
|
|
782
|
+
* the model's CATEGORY leaf, not on the product, so it needs a read — and
|
|
783
|
+
* `expandCartLineToOrderRows` is pure by design.
|
|
784
|
+
*
|
|
785
|
+
* 🛑 Usually free: it returns immediately unless some line's listing type
|
|
786
|
+
* has a composition at all, and then reads only the DISTINCT leaves,
|
|
787
|
+
* capped. It never throws — a parts list we could not read must not fail a
|
|
788
|
+
* payment (see the module header).
|
|
789
|
+
*/
|
|
790
|
+
const orderItems = await freezePartsOnItems(baseOrderItems);
|
|
644
791
|
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
645
792
|
/*
|
|
646
793
|
* D44: the group's real goods value, so free-above-threshold can fire.
|
|
@@ -650,7 +797,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
650
797
|
* grouped line the two differ.
|
|
651
798
|
*/
|
|
652
799
|
const groupGoodsValue = group.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0);
|
|
653
|
-
const { shippingFee: resolvedShippingFee, storeOwnerId, storeEmiEnabled, storeState, storeProtections, storeTaxIdentity } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupGoodsValue });
|
|
800
|
+
const { shippingFee: resolvedShippingFee, storeOwnerId, storeEmiEnabled, storeState, storeProtections, storeTaxIdentity, acceptsPoints, sharedPurchaseRates } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupGoodsValue });
|
|
654
801
|
/*
|
|
655
802
|
* 🛑 The buyer's quote WINS over the seller's configured rate, and it is
|
|
656
803
|
* re-rounded here rather than trusted as sent.
|
|
@@ -699,14 +846,30 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
699
846
|
* simply did not exist yet. The split now sits immediately after
|
|
700
847
|
* `orderTotal`, which is the only place every term is known.
|
|
701
848
|
*/
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
849
|
+
/*
|
|
850
|
+
* 🛑 Keyed on `emiOptIn`, NOT on the tender. An EMI order's `paymentMethod`
|
|
851
|
+
* is now whatever the buyer picked to pay the token with — `online`,
|
|
852
|
+
* `upi_manual` or `cash` — so testing `paymentMethod === "emi"` here would
|
|
853
|
+
* build a schedule for nobody and every instalment plan would silently
|
|
854
|
+
* vanish into a full-price order.
|
|
855
|
+
*/
|
|
856
|
+
const emiGroupInput = {
|
|
857
|
+
emiOptIn,
|
|
858
|
+
emiTenureMonths,
|
|
859
|
+
groupSubtotal: groupTotal,
|
|
860
|
+
sellerEmiEnabled: storeEmiEnabled,
|
|
861
|
+
};
|
|
862
|
+
/*
|
|
863
|
+
* 🛑 REFUSES on an ineligible EMI order rather than quietly dropping the
|
|
864
|
+
* plan. The preview deliberately does the opposite — see `groupEmiGate`'s
|
|
865
|
+
* docstring. Silently producing a full-price order from an EMI request is
|
|
866
|
+
* money the buyer did not agree to.
|
|
867
|
+
*/
|
|
868
|
+
const emiGate = groupEmiGate(emiGroupInput, emiSettings);
|
|
869
|
+
if (emiOptIn && emiTenureMonths && !emiGate.applies) {
|
|
870
|
+
throw new ValidationError(`EMI is not available for this order (${emiGate.reason}). Minimum order value per seller is ₹${emiSettings.minOrderValue}.`);
|
|
709
871
|
}
|
|
872
|
+
const emiSchedule = groupEmiSchedule(emiGroupInput, emiSettings) ?? undefined;
|
|
710
873
|
// Handling fee charged to the buyer for choosing COD, on top of the order
|
|
711
874
|
// total — not deducted from the deposit/remaining split above.
|
|
712
875
|
const codHandlingFee = paymentMethod === "cod" ? computeCodHandlingFee(groupTotal, commissions) : 0;
|
|
@@ -818,6 +981,47 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
818
981
|
groupCouponCodes.add(discount.code);
|
|
819
982
|
}
|
|
820
983
|
}
|
|
984
|
+
/*
|
|
985
|
+
* ── S5a: a shared purchase — one payer, N destinations ────────────────────
|
|
986
|
+
*
|
|
987
|
+
* Computed BEFORE `orderTotal` because it is a term in it. The rates come
|
|
988
|
+
* off the store document and are SNAPSHOTTED onto the order below: a
|
|
989
|
+
* coordinator commits against a quoted total, and re-reading a rate at
|
|
990
|
+
* settle would let a seller raise it afterwards. That is D61's rule, and the
|
|
991
|
+
* precedent is the refund path reading `finalSale` from the order snapshot
|
|
992
|
+
* for exactly this reason.
|
|
993
|
+
*
|
|
994
|
+
* 🛑 `countDestinations` counts DISTINCT addresses and collapses every line
|
|
995
|
+
* with no `destinationAddressId` into one bucket — the order's own address.
|
|
996
|
+
* Counting items instead would charge an ordinary three-item order for two
|
|
997
|
+
* extra legs it never asked for.
|
|
998
|
+
*/
|
|
999
|
+
const sharedQuote = quoteSharedPurchase({
|
|
1000
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
1001
|
+
rates: sharedPurchaseRates,
|
|
1002
|
+
});
|
|
1003
|
+
/*
|
|
1004
|
+
* 🛑 An UNPRICED arrangement is refused, not given away. The seller opted in
|
|
1005
|
+
* without setting a rate, which is not consent to do the extra packing for
|
|
1006
|
+
* free — and quoting ₹0 is exactly D44: `shippingUnavailable` carried a
|
|
1007
|
+
* docstring saying "the caller MUST act on this rather than trusting
|
|
1008
|
+
* `shippingFee: 0`", had zero consumers, and every `estimate`-mode store
|
|
1009
|
+
* shipped free on every path as a result.
|
|
1010
|
+
*/
|
|
1011
|
+
if (sharedQuote.unpriced) {
|
|
1012
|
+
throw new ValidationError("This seller has not set a price for splitting an order across addresses. " +
|
|
1013
|
+
"Send everything to one address, or contact the seller.");
|
|
1014
|
+
}
|
|
1015
|
+
/*
|
|
1016
|
+
* A multi-destination order from a store that has NOT opted in. The quote
|
|
1017
|
+
* prices; it does not authorise — so the refusal is here, where the buyer
|
|
1018
|
+
* can be told something true and specific.
|
|
1019
|
+
*/
|
|
1020
|
+
if (!sharedQuote.isShared &&
|
|
1021
|
+
countDestinations(group.map(({ item }) => item)) > 1) {
|
|
1022
|
+
throw new ValidationError("This seller ships one order to one address. Remove the extra delivery addresses, " +
|
|
1023
|
+
"or order the items separately.");
|
|
1024
|
+
}
|
|
821
1025
|
// The platform commission is now charged on EVERY payment method. It used to
|
|
822
1026
|
// be added only on the online-payment path, so COD / UPI-manual / cash / EMI
|
|
823
1027
|
// buyers silently never paid it.
|
|
@@ -848,7 +1052,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
848
1052
|
* compounds nine half-paisa errors instead of one, which is how a total
|
|
849
1053
|
* drifts from the sum of the lines shown beside it.
|
|
850
1054
|
*/
|
|
851
|
-
roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0));
|
|
1055
|
+
roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0) + sharedQuote.total);
|
|
852
1056
|
/*
|
|
853
1057
|
* ── D43: the COD deposit/remaining split, on the ORDER TOTAL ──────────────
|
|
854
1058
|
*
|
|
@@ -891,16 +1095,72 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
891
1095
|
// Deadline + displayed UPI apply only to methods with a buyer-uploaded
|
|
892
1096
|
// proof step — never cod (paid on delivery), online (paid before order
|
|
893
1097
|
// creation), or admin_bypass (already marked paid above).
|
|
1098
|
+
/*
|
|
1099
|
+
* 🛑 `PaymentMethodValues.EMI` was a third arm here and is gone on purpose,
|
|
1100
|
+
* not by oversight. It is unreachable after `resolveEmiChoice` — an EMI
|
|
1101
|
+
* order's tender is `online` / `upi_manual` / `cash` — and leaving it would
|
|
1102
|
+
* assert that "EMI" is a way of paying, which is exactly the conflation 0f
|
|
1103
|
+
* removed. An EMI buyer paying by bank transfer still gets the window,
|
|
1104
|
+
* because their tender IS `upi_manual`; one paying online does not, because
|
|
1105
|
+
* the token is captured before the order exists.
|
|
1106
|
+
*/
|
|
894
1107
|
const hasPaymentWindow = !adminBypass &&
|
|
895
1108
|
(paymentMethod === PaymentMethodValues.UPI_MANUAL ||
|
|
896
|
-
paymentMethod === PaymentMethodValues.CASH
|
|
897
|
-
paymentMethod === PaymentMethodValues.EMI);
|
|
1109
|
+
paymentMethod === PaymentMethodValues.CASH);
|
|
898
1110
|
const paymentDeadline = hasPaymentWindow
|
|
899
1111
|
? new Date(Date.now() + PAYMENT_WINDOW_MS)
|
|
900
1112
|
: undefined;
|
|
901
1113
|
const displayedUpiId = hasPaymentWindow
|
|
902
1114
|
? await resolveDisplayedUpiId(firstItem.storeId, siteContactUpiVpa)
|
|
903
1115
|
: undefined;
|
|
1116
|
+
/*
|
|
1117
|
+
* ── POINTS TENDER (D46/D47) — a STAGE, after `orderTotal` is rounded ──────
|
|
1118
|
+
*
|
|
1119
|
+
* The deleted code redeemed points AFTER the orders existed and after the
|
|
1120
|
+
* gateway had captured `orderTotal`, so a successful redemption took points
|
|
1121
|
+
* from a buyer who had just paid full price — while its own comment claimed
|
|
1122
|
+
* "the balance intact". This is the same feature done as a pipeline stage:
|
|
1123
|
+
* `amountDue` is derived here, and `amountDue` is what the gateway captures
|
|
1124
|
+
* and what the post-capture guard compares.
|
|
1125
|
+
*
|
|
1126
|
+
* 🛑 `useAllPoints` is the only client input and it is a BOOLEAN (decision
|
|
1127
|
+
* 24). The amount comes from the live balance and the 50% cap; a
|
|
1128
|
+
* client-supplied money figure is a client-supplied discount.
|
|
1129
|
+
*
|
|
1130
|
+
* 🛑 Per group, because the ACCEPTING store absorbs the redemption against
|
|
1131
|
+
* its own payout (decision 3). A cart spanning an accepting and a
|
|
1132
|
+
* non-accepting store must not pool the discount — so this reads
|
|
1133
|
+
* `acceptsPoints`, which `resolveShippingCost` resolved from the store
|
|
1134
|
+
* document it had already fetched.
|
|
1135
|
+
*
|
|
1136
|
+
* 🛑 `pointsBudget` is a SHARED MUTABLE REMAINDER, read once per checkout by
|
|
1137
|
+
* the caller and decremented here — the same shape as
|
|
1138
|
+
* `couponUsageAccumulator` and `platformFeeByStore`. Reading the balance
|
|
1139
|
+
* per group would let two groups each spend the whole balance: `redeem`
|
|
1140
|
+
* would then refuse the second one inside its transaction, leaving an order
|
|
1141
|
+
* recorded at a discount that was never funded. One budget, drawn down in
|
|
1142
|
+
* order, is what makes the sum of the groups equal what the buyer has.
|
|
1143
|
+
*/
|
|
1144
|
+
const pointsOptedIn = useAllPoints === true;
|
|
1145
|
+
const pointsAccepted = pointsOptedIn && acceptsPoints;
|
|
1146
|
+
const { pointsApplied, amountDue } = resolvePointsTender({
|
|
1147
|
+
orderTotal,
|
|
1148
|
+
balance: pointsAccepted ? pointsBudget.remaining : 0,
|
|
1149
|
+
accepted: pointsAccepted,
|
|
1150
|
+
optedIn: pointsOptedIn,
|
|
1151
|
+
});
|
|
1152
|
+
if (pointsApplied > 0)
|
|
1153
|
+
pointsBudget.remaining -= pointsApplied;
|
|
1154
|
+
/*
|
|
1155
|
+
* What is COLLECTED, composing both tenders that reduce it. On a plain order
|
|
1156
|
+
* this equals `orderTotal`, so the field is omitted below and every
|
|
1157
|
+
* `amountDue ?? totalPrice` reader is unaffected.
|
|
1158
|
+
*/
|
|
1159
|
+
const amountDueOnOrder = amountDueNow({
|
|
1160
|
+
orderTotal,
|
|
1161
|
+
pointsApplied,
|
|
1162
|
+
emiRemainingBalance: emiSchedule?.remainingBalance ?? 0,
|
|
1163
|
+
});
|
|
904
1164
|
// Both creation paths call the SAME builder — two hand-written copies is the
|
|
905
1165
|
// create-vs-create axis of Root Cause #39, and these two functions have
|
|
906
1166
|
// already drifted once before (add-on fees, Root Cause #65).
|
|
@@ -914,6 +1174,39 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
914
1174
|
quantity: totalQuantity,
|
|
915
1175
|
unitPrice: unitPriceFor(firstItem, firstProduct),
|
|
916
1176
|
totalPrice: orderTotal,
|
|
1177
|
+
/*
|
|
1178
|
+
* D46/D47 — what the points paid for, and what is left to collect.
|
|
1179
|
+
*
|
|
1180
|
+
* 🛑 `totalPrice` stays the FULL order value and is not reduced. It is the
|
|
1181
|
+
* value of the supply, it is what the GST breakdown above was computed on,
|
|
1182
|
+
* and it is what `refundableBase` measures against — netting the points
|
|
1183
|
+
* off it would silently shrink the refundable base and the taxable value
|
|
1184
|
+
* at once. Points are a DISCOUNT settled against goods (decision 5), so
|
|
1185
|
+
* they reduce the amount COLLECTED, not the amount supplied.
|
|
1186
|
+
*
|
|
1187
|
+
* 🛑 Both are written only when non-zero, so an order placed without
|
|
1188
|
+
* points carries neither field and every reader's `amountDue ?? totalPrice`
|
|
1189
|
+
* fallback resolves to the right number on the 100% of historical orders
|
|
1190
|
+
* that predate this.
|
|
1191
|
+
*/
|
|
1192
|
+
pointsApplied: pointsApplied > 0 ? pointsApplied : undefined,
|
|
1193
|
+
/*
|
|
1194
|
+
* 🛑 WHAT IS COLLECTED NOW, and on an EMI order that is the token — not
|
|
1195
|
+
* the whole total.
|
|
1196
|
+
*
|
|
1197
|
+
* This was `pointsApplied > 0 ? amountDue : undefined`, so an EMI order
|
|
1198
|
+
* carried no `amountDue` at all and every reader fell back to
|
|
1199
|
+
* `order.total`. The buyer's manual-payment page therefore asked an EMI
|
|
1200
|
+
* buyer to transfer the FULL order value up front and then showed them a
|
|
1201
|
+
* schedule of monthly instalments: `emiTokenAmount` was computed, stored,
|
|
1202
|
+
* and read by nothing (measured — zero readers across both repos).
|
|
1203
|
+
*
|
|
1204
|
+
* `amountDueNow` subtracts the deferred balance as well as the points, so
|
|
1205
|
+
* the two tenders compose. Written only when it differs from the total, so
|
|
1206
|
+
* every historical order keeps resolving through the same
|
|
1207
|
+
* `amountDue ?? totalPrice` fallback its readers already use.
|
|
1208
|
+
*/
|
|
1209
|
+
amountDue: amountDueOnOrder !== orderTotal ? amountDueOnOrder : undefined,
|
|
917
1210
|
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
918
1211
|
storeId: firstItem.storeId || undefined,
|
|
919
1212
|
storeName: firstItem.storeName || undefined,
|
|
@@ -946,6 +1239,12 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
946
1239
|
displayedUpiId,
|
|
947
1240
|
adminBypassBy: adminBypassBy,
|
|
948
1241
|
shippingAddress,
|
|
1242
|
+
/*
|
|
1243
|
+
* 🛑 Spread BEFORE `placeOfSupply`, which now prefers the snapshot's
|
|
1244
|
+
* state. Spreading after would let an absent snapshot overwrite a
|
|
1245
|
+
* state the digital-cart fallback had already resolved.
|
|
1246
|
+
*/
|
|
1247
|
+
...(deliverySnapshot ?? {}),
|
|
949
1248
|
notes,
|
|
950
1249
|
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
951
1250
|
depositAmount: adminBypass ? undefined : depositAmount,
|
|
@@ -971,11 +1270,39 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
971
1270
|
shipmentProtectionFee: !adminBypass && shipmentProtectionFee > 0 ? shipmentProtectionFee : undefined,
|
|
972
1271
|
saleProtectionAddon: !adminBypass && saleProtectionFee > 0 ? true : undefined,
|
|
973
1272
|
saleProtectionFee: !adminBypass && saleProtectionFee > 0 ? saleProtectionFee : undefined,
|
|
1273
|
+
/*
|
|
1274
|
+
* S5a — the arrangement, snapshotted. Written only when it IS a shared
|
|
1275
|
+
* purchase, so an ordinary order carries no field at all and every reader
|
|
1276
|
+
* can test presence rather than compare a count to 1.
|
|
1277
|
+
*
|
|
1278
|
+
* 🛑 The FEES AS CHARGED, not the store's current rates. That is the whole
|
|
1279
|
+
* point of a snapshot: an order refunded in six months must reverse what
|
|
1280
|
+
* was collected, not what the seller charges by then.
|
|
1281
|
+
*/
|
|
1282
|
+
sharedPurchase: sharedQuote.isShared
|
|
1283
|
+
? {
|
|
1284
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
1285
|
+
handlingFee: sharedQuote.handlingFee,
|
|
1286
|
+
extraShippingFee: sharedQuote.extraShippingFee,
|
|
1287
|
+
}
|
|
1288
|
+
: undefined,
|
|
974
1289
|
taxableAmount: gstBreakdown?.taxableAmount,
|
|
975
1290
|
/* Exempt goods are a reportable nil-rated supply, kept OUT of
|
|
976
1291
|
* `taxableAmount` so the taxed base stays the base the tax was charged on,
|
|
977
1292
|
* and recorded here so the invoice's columns can reconcile. */
|
|
978
1293
|
exemptAmount: gstBreakdown?.exemptAmount || undefined,
|
|
1294
|
+
/*
|
|
1295
|
+
* Phase 7 — the place of supply, which is the INPUT the CGST/SGST-vs-IGST
|
|
1296
|
+
* split above was computed from. Only the split's RESULT was recorded
|
|
1297
|
+
* before, so an order could not state why it was taxed the way it was, and
|
|
1298
|
+
* GSTR-1's mandatory place-of-supply column had nothing to read.
|
|
1299
|
+
*/
|
|
1300
|
+
/*
|
|
1301
|
+
* Part 14 — the snapshot's state is authoritative, because it is the
|
|
1302
|
+
* field the invoice and GSTR-1 actually read. `buyerState` stays as
|
|
1303
|
+
* the fallback for a digital cart, which has no delivery address.
|
|
1304
|
+
*/
|
|
1305
|
+
placeOfSupply: deliverySnapshot?.deliveryState || buyerState || undefined,
|
|
979
1306
|
/* The supplier of record AS IT STANDS NOW. A tax invoice without the
|
|
980
1307
|
* supplier's GSTIN is not a tax invoice, and the invoice page cannot read
|
|
981
1308
|
* it from site settings — `gst` is in PRIVATE_SITE_FIELDS. See the schema.
|
|
@@ -1013,23 +1340,19 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
1013
1340
|
cgst: gstBreakdown?.cgst || undefined,
|
|
1014
1341
|
sgst: gstBreakdown?.sgst || undefined,
|
|
1015
1342
|
igst: gstBreakdown?.igst || undefined,
|
|
1016
|
-
emiEnabled: !adminBypass && !!emiSchedule ? true : undefined,
|
|
1017
|
-
emiTenureMonths: !adminBypass && emiSchedule ? emiTenureMonths : undefined,
|
|
1018
|
-
emiTokenAmount: !adminBypass ? emiSchedule?.tokenAmount : undefined,
|
|
1019
|
-
emiSurchargeAmount: !adminBypass ? emiSchedule?.surchargeAmount : undefined,
|
|
1020
1343
|
/*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1344
|
+
* The eleven `emi*` fields, from one builder shared with the gateway
|
|
1345
|
+
* path — which carried none of them, so every order paid online lost its
|
|
1346
|
+
* instalment plan. D45's note about `emiSurchargeSellerShare` being
|
|
1347
|
+
* computed and discarded now lives on `emiOrderFields` itself.
|
|
1348
|
+
*
|
|
1349
|
+
* 🛑 `adminBypass` nulls the whole group by passing no schedule, rather
|
|
1350
|
+
* than by repeating `!adminBypass &&` eleven times. The bypass marks an
|
|
1351
|
+
* order paid without a payment, so there is no token and no plan — and
|
|
1352
|
+
* `resolveEmiChoice` now refuses that combination outright, making this
|
|
1353
|
+
* a second line of defence rather than the only one.
|
|
1026
1354
|
*/
|
|
1027
|
-
|
|
1028
|
-
emiInstallments: !adminBypass && emiSchedule
|
|
1029
|
-
? emiSchedule.installments.map((i) => ({ index: i.index, dueDate: i.dueDate, amount: i.amount, status: "pending" }))
|
|
1030
|
-
: undefined,
|
|
1031
|
-
emiRemainingBalance: !adminBypass ? emiSchedule?.remainingBalance : undefined,
|
|
1032
|
-
emiComplete: !adminBypass && emiSchedule ? false : undefined,
|
|
1355
|
+
...emiOrderFields(adminBypass ? null : (emiSchedule ?? null), emiTenureMonths),
|
|
1033
1356
|
couponCode: appliedDiscounts[0]?.code,
|
|
1034
1357
|
couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
|
|
1035
1358
|
appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
|
|
@@ -1039,6 +1362,9 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
1039
1362
|
...extraOrderFields,
|
|
1040
1363
|
});
|
|
1041
1364
|
orderIds.push(order.id);
|
|
1365
|
+
// The second half of the points tender — see `settlePointsTender`. Runs
|
|
1366
|
+
// before the caller asks the gateway for anything.
|
|
1367
|
+
await settlePointsTender({ orderId: order.id, userId: uid, pointsApplied, pointsBudget });
|
|
1042
1368
|
// Close the loop on any locked line this order settled: flip the offer to
|
|
1043
1369
|
// "paid" (its terminal status had NO server-side writer before 2026-08-21,
|
|
1044
1370
|
// so an accepted offer could be re-added and re-ordered) and back-link the
|
|
@@ -1089,15 +1415,17 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
1089
1415
|
* auth + rate-limiting before calling this and supply the resolved user fields.
|
|
1090
1416
|
*/
|
|
1091
1417
|
export async function createCheckoutOrderAction(input) {
|
|
1092
|
-
const { userId: uid, userName, userEmail, addressId,
|
|
1418
|
+
const { userId: uid, userName, userEmail, addressId, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, outOfStockPolicy = OutOfStockPolicyValues.SKIP_ITEMS, } = input;
|
|
1419
|
+
/*
|
|
1420
|
+
* The TENDER, resolved before anything else because the COD and manual-UPI
|
|
1421
|
+
* guards below are about the tender and run before the cart read. The
|
|
1422
|
+
* ARRANGEMENT (`emiOptIn` + tenure) lives on the cart and is resolved after
|
|
1423
|
+
* it — see the block below the cart load.
|
|
1424
|
+
*/
|
|
1425
|
+
const paymentMethod = emiTenderFor(input.paymentMethod);
|
|
1093
1426
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
1094
1427
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
1095
1428
|
const emiSettings = siteSettings?.emi ?? CHECKOUT_DEFAULT_EMI_SETTINGS;
|
|
1096
|
-
if (paymentMethod === "emi") {
|
|
1097
|
-
if (!emiTenureMonths || !emiSettings.tenureOptions.includes(emiTenureMonths)) {
|
|
1098
|
-
throw new ValidationError(`emiTenureMonths must be one of: ${emiSettings.tenureOptions.join(", ")}`);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
1429
|
// W1-43 — enforce COD toggle.
|
|
1102
1430
|
if (!adminBypass &&
|
|
1103
1431
|
paymentMethod === "cod" &&
|
|
@@ -1114,6 +1442,30 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1114
1442
|
if (!cart.items || cart.items.length === 0) {
|
|
1115
1443
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
1116
1444
|
}
|
|
1445
|
+
/*
|
|
1446
|
+
* ── The two payment axes, resolved ONCE, AFTER the cart is loaded ────────
|
|
1447
|
+
*
|
|
1448
|
+
* Everything below reads `paymentMethod` (the tender) and `emiOptIn` (the
|
|
1449
|
+
* arrangement) from here, never from `input`. A second place deciding "is
|
|
1450
|
+
* this an EMI order" is the drift this normaliser exists to prevent, and
|
|
1451
|
+
* the legacy `paymentMethod: "emi"` mapping only holds if exactly one
|
|
1452
|
+
* expression performs it.
|
|
1453
|
+
*
|
|
1454
|
+
* 🛑 It has to sit below the cart read because the plan LIVES on the cart,
|
|
1455
|
+
* not in the request — see `CartDocument.emiPlan`. The cart is loaded here
|
|
1456
|
+
* anyway, so this costs nothing.
|
|
1457
|
+
*/
|
|
1458
|
+
const emiChoice = resolveEmiChoice({
|
|
1459
|
+
paymentMethod: input.paymentMethod,
|
|
1460
|
+
cartEmiPlan: cart.emiPlan ?? null,
|
|
1461
|
+
legacyTenureMonths: input.emiTenureMonths,
|
|
1462
|
+
});
|
|
1463
|
+
if (!emiChoice.ok)
|
|
1464
|
+
throw new ValidationError(emiChoice.reason);
|
|
1465
|
+
const { emiOptIn, emiTenureMonths } = emiChoice.choice;
|
|
1466
|
+
if (emiOptIn && (!emiTenureMonths || !emiSettings.tenureOptions.includes(emiTenureMonths))) {
|
|
1467
|
+
throw new ValidationError(`emiTenureMonths must be one of: ${emiSettings.tenureOptions.join(", ")}`);
|
|
1468
|
+
}
|
|
1117
1469
|
const excludedSet = new Set(excludedProductIds);
|
|
1118
1470
|
const selectedSet = cart.selectedItemIds?.length
|
|
1119
1471
|
? new Set(cart.selectedItemIds)
|
|
@@ -1136,16 +1488,13 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1136
1488
|
// otherwise a buyer could dodge it by splitting a big cart across
|
|
1137
1489
|
// sellers. Skipped for COD (physical hand-off at the door is its own
|
|
1138
1490
|
// verification) and admin-bypass checkouts.
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1491
|
+
await assertCheckoutValueOtp({
|
|
1492
|
+
uid,
|
|
1493
|
+
amount: cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
1494
|
+
threshold: siteSettings?.payment?.otpCheckoutThreshold,
|
|
1495
|
+
paymentMethod,
|
|
1496
|
+
adminBypass,
|
|
1497
|
+
});
|
|
1149
1498
|
/*
|
|
1150
1499
|
* COD value cap — the SERVER half of the gate.
|
|
1151
1500
|
*
|
|
@@ -1181,6 +1530,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1181
1530
|
}
|
|
1182
1531
|
const isDigitalCart = cartIsDigitalOnly(cartItems);
|
|
1183
1532
|
let shippingAddress;
|
|
1533
|
+
let deliverySnapshot;
|
|
1184
1534
|
let resolvedAddress = null;
|
|
1185
1535
|
if (!isDigitalCart) {
|
|
1186
1536
|
if (!addressId) {
|
|
@@ -1201,6 +1551,13 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1201
1551
|
throw new NotFoundError(ERROR_MESSAGES.CHECKOUT.ADDRESS_REQUIRED);
|
|
1202
1552
|
}
|
|
1203
1553
|
shippingAddress = formatShippingAddress(resolvedAddress);
|
|
1554
|
+
/*
|
|
1555
|
+
* 🛑 REVEALED first. `findById` returns this PII encrypted, and
|
|
1556
|
+
* `snapshotDeliveryAddress` demands `AddressPlain` precisely so that
|
|
1557
|
+
* freezing `enc:v1:…` onto an invoice is a compile error rather than
|
|
1558
|
+
* something discovered on a printed label.
|
|
1559
|
+
*/
|
|
1560
|
+
deliverySnapshot = snapshotDeliveryAddress(unitOfWork.addresses.revealAddress(resolvedAddress));
|
|
1204
1561
|
}
|
|
1205
1562
|
const db = getAdminDb();
|
|
1206
1563
|
// SB6-C — pre-tx fetch products so we can run the maxPerUser cap check
|
|
@@ -1240,6 +1597,19 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1240
1597
|
* is the only safe place for this, and the method is decided here.
|
|
1241
1598
|
*/
|
|
1242
1599
|
assertPaymentMethodAllowed(cartItems, preTxProductById, paymentMethod);
|
|
1600
|
+
/*
|
|
1601
|
+
* 🛑 The second axis needs its own assertion, and omitting it would have
|
|
1602
|
+
* been a silent regression of a rule that already exists.
|
|
1603
|
+
*
|
|
1604
|
+
* `prizeDrawRule.blockedPaymentMethods` lists `"emi"` because winner
|
|
1605
|
+
* assignment waits on confirmed payment. Once EMI stopped being a tender,
|
|
1606
|
+
* that entry stopped matching anything — the gate above would see
|
|
1607
|
+
* `upi_manual` (explicitly ALLOWED for prize draws) and wave through an
|
|
1608
|
+
* instalment plan on a listing type that forbids one. This reads the SAME
|
|
1609
|
+
* declaration rather than a second list, so a rule written once still
|
|
1610
|
+
* governs both axes.
|
|
1611
|
+
*/
|
|
1612
|
+
assertEmiOptInAllowed(cartItems, preTxProductById, emiOptIn);
|
|
1243
1613
|
const preTxPairs = cartItems
|
|
1244
1614
|
.map((item) => {
|
|
1245
1615
|
const [firstMember] = getCartItemMemberIds(item);
|
|
@@ -1388,8 +1758,27 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1388
1758
|
g[0].item.storeId,
|
|
1389
1759
|
g.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0),
|
|
1390
1760
|
]), computeCheckoutFees(cartSubtotal, commissions));
|
|
1761
|
+
/*
|
|
1762
|
+
* ── The checkout's points budget, read ONCE (D46/D47) ─────────────────────
|
|
1763
|
+
*
|
|
1764
|
+
* 🛑 One read for the whole checkout, not one per order group. Rule #6
|
|
1765
|
+
* budgets ~3 sequential Firestore round-trips per request, and a cart
|
|
1766
|
+
* spanning four sellers would otherwise pay four reads to learn one number.
|
|
1767
|
+
*
|
|
1768
|
+
* 🛑 And ZERO when the buyer did not opt in, so an ordinary checkout performs
|
|
1769
|
+
* no points read at all — the common case must not pay for a feature it is
|
|
1770
|
+
* not using.
|
|
1771
|
+
*
|
|
1772
|
+
* 🛑 `adminBypass` is deliberately excluded: that path takes no payment, so
|
|
1773
|
+
* there is nothing for points to reduce, and debiting a buyer's balance for
|
|
1774
|
+
* an order the admin placed free would be a theft with an audit-log entry
|
|
1775
|
+
* saying otherwise.
|
|
1776
|
+
*/
|
|
1777
|
+
const useAllPoints = cart.useAllPoints === true && !adminBypass;
|
|
1778
|
+
const pointsBudget = { remaining: useAllPoints ? await safePointsBalance(uid) : 0 };
|
|
1391
1779
|
const groupCtx = {
|
|
1392
1780
|
paymentMethod,
|
|
1781
|
+
emiOptIn,
|
|
1393
1782
|
emiTenureMonths,
|
|
1394
1783
|
emiSettings,
|
|
1395
1784
|
commissions,
|
|
@@ -1400,6 +1789,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1400
1789
|
userName,
|
|
1401
1790
|
userEmail,
|
|
1402
1791
|
shippingAddress,
|
|
1792
|
+
deliverySnapshot,
|
|
1403
1793
|
notes,
|
|
1404
1794
|
adminBypass,
|
|
1405
1795
|
adminBypassBy,
|
|
@@ -1415,6 +1805,8 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1415
1805
|
platformFeeByStore,
|
|
1416
1806
|
selectedShippingQuote: input.selectedShippingQuote,
|
|
1417
1807
|
productById: checkoutProductById,
|
|
1808
|
+
useAllPoints,
|
|
1809
|
+
pointsBudget,
|
|
1418
1810
|
};
|
|
1419
1811
|
for (const { items: group, orderType } of orderGroups) {
|
|
1420
1812
|
total += await createOrderForGroup(group, orderType, groupCtx);
|
|
@@ -1448,18 +1840,26 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1448
1840
|
* Deleting it leaves a promise with no mechanism, which is a disappointment.
|
|
1449
1841
|
* Keeping it left a mechanism that silently takes points, which is a theft.
|
|
1450
1842
|
*
|
|
1451
|
-
* ##
|
|
1452
|
-
*
|
|
1453
|
-
* Points are a TENDER, so they
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1843
|
+
* ## ✅ The real tender was BUILT 2026-10-22 — and it is not here
|
|
1844
|
+
*
|
|
1845
|
+
* 🛑 Do not look for it at this line. Points are a TENDER, so they are a
|
|
1846
|
+
* STAGE inside each order-creation path, immediately after that path's single
|
|
1847
|
+
* `roundRupees` and BEFORE anything is written or captured:
|
|
1848
|
+
*
|
|
1849
|
+
* - `resolvePointsTender` (`_internal/shared/checkout/order-math.ts`) —
|
|
1850
|
+
* the arithmetic, beside every other money rule.
|
|
1851
|
+
* - the stage in `createOrderForGroup` and `createPhonePeGroupOrder`,
|
|
1852
|
+
* which write `pointsApplied` + `amountDue` onto the order.
|
|
1853
|
+
* - `settlePointsTender` — ONE debit implementation shared by both paths,
|
|
1854
|
+
* running after the order exists so the discount and the debit are two
|
|
1855
|
+
* halves of one recorded fact, and reverting the discount if `redeem`
|
|
1856
|
+
* refuses.
|
|
1857
|
+
*
|
|
1858
|
+
* This note stays because the mistake is worth keeping, but a comment saying
|
|
1859
|
+
* a feature is "missing" while it sits 800 lines above is its own trap — it
|
|
1860
|
+
* is how `assertReturnWindowOpen` got reported as absent when my own comment
|
|
1861
|
+
* was shadowing the real answer. The accurate statement is: the tender is
|
|
1862
|
+
* built, and this is the wrong place for it.
|
|
1463
1863
|
*/
|
|
1464
1864
|
dispatchOrderConfirmationEmails(emailsToSend);
|
|
1465
1865
|
serverLogger.info(`createCheckoutOrderAction: ${orderIds.length} order(s) placed for uid=${uid}` +
|
|
@@ -1489,11 +1889,16 @@ const EMPTY_PRICING_PREVIEW = {
|
|
|
1489
1889
|
shipmentProtectionFee: 0,
|
|
1490
1890
|
saleProtectionFee: 0,
|
|
1491
1891
|
emiSurcharge: 0,
|
|
1892
|
+
emiDeferred: 0,
|
|
1492
1893
|
gstAmount: 0,
|
|
1493
1894
|
couponDiscount: 0,
|
|
1494
1895
|
platformFee: 0,
|
|
1495
1896
|
gstOnFee: 0,
|
|
1897
|
+
sharedPurchaseFee: 0,
|
|
1496
1898
|
total: 0,
|
|
1899
|
+
pointsApplied: 0,
|
|
1900
|
+
amountDue: 0,
|
|
1901
|
+
pointsBalance: 0,
|
|
1497
1902
|
stores: [],
|
|
1498
1903
|
};
|
|
1499
1904
|
/**
|
|
@@ -1506,10 +1911,29 @@ const EMPTY_PRICING_PREVIEW = {
|
|
|
1506
1911
|
* stock, creates an order, or writes coupon usage.
|
|
1507
1912
|
*/
|
|
1508
1913
|
export async function previewCheckoutPricing(input) {
|
|
1509
|
-
const { userId: uid, addressId,
|
|
1914
|
+
const { userId: uid, addressId, excludedProductIds = [], lane, } = input;
|
|
1915
|
+
/*
|
|
1916
|
+
* 🛑 Same normaliser as `createCheckoutOrderAction`, and on an INCOMPATIBLE
|
|
1917
|
+
* combination this prices the tender WITHOUT instalments rather than
|
|
1918
|
+
* throwing.
|
|
1919
|
+
*
|
|
1920
|
+
* The preview exists so a buyer can see consequences while they fiddle with
|
|
1921
|
+
* controls; a 500 in the middle of that blanks the Order Summary and reads
|
|
1922
|
+
* as a broken page. The order action still refuses, which is where refusing
|
|
1923
|
+
* belongs — the same split this function already makes for EMI eligibility
|
|
1924
|
+
* two hundred lines below.
|
|
1925
|
+
*/
|
|
1926
|
+
const paymentMethod = emiTenderFor(input.paymentMethod);
|
|
1510
1927
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
1511
1928
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
1512
1929
|
const cart = await unitOfWork.carts.getOrCreate(uid);
|
|
1930
|
+
const emiChoice = resolveEmiChoice({
|
|
1931
|
+
paymentMethod: input.paymentMethod,
|
|
1932
|
+
cartEmiPlan: cart.emiPlan ?? null,
|
|
1933
|
+
legacyTenureMonths: input.emiTenureMonths,
|
|
1934
|
+
});
|
|
1935
|
+
const emiOptIn = emiChoice.ok && emiChoice.choice.emiOptIn;
|
|
1936
|
+
const emiTenureMonths = emiChoice.ok ? emiChoice.choice.emiTenureMonths : undefined;
|
|
1513
1937
|
const excludedSet = new Set(excludedProductIds);
|
|
1514
1938
|
const selectedSet = cart.selectedItemIds?.length ? new Set(cart.selectedItemIds) : null;
|
|
1515
1939
|
// An explicit lane wins: the cart page prices whichever tab is open, which may
|
|
@@ -1566,7 +1990,7 @@ export async function previewCheckoutPricing(input) {
|
|
|
1566
1990
|
* and then the order path (equally unpassed) charged it. Preview and charge
|
|
1567
1991
|
* agreed, and both were wrong in the same direction.
|
|
1568
1992
|
*/
|
|
1569
|
-
const { shippingFee: groupShippingFee, storeState, storeProtections, storeEmiEnabled } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
1993
|
+
const { shippingFee: groupShippingFee, storeState, storeProtections, storeEmiEnabled, acceptsPoints, sharedPurchaseRates: previewSharedRates } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
1570
1994
|
// Add-ons are per store, read off the cart document — the same entry
|
|
1571
1995
|
// createOrderForGroup will read when the order is actually placed.
|
|
1572
1996
|
const addons = cart.storeAddons?.[firstItem.storeId] ?? {};
|
|
@@ -1597,13 +2021,14 @@ export async function previewCheckoutPricing(input) {
|
|
|
1597
2021
|
* ineligible EMI order, which is where refusing belongs. An ineligible
|
|
1598
2022
|
* cart simply previews no surcharge, because the tenure never applies.
|
|
1599
2023
|
*/
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
2024
|
+
const groupEmiPreviewQuote = groupEmiQuote({ emiOptIn, emiTenureMonths, groupSubtotal: groupTotal, sellerEmiEnabled: storeEmiEnabled }, siteSettings?.emi);
|
|
2025
|
+
const groupEmiSurcharge = groupEmiPreviewQuote?.surchargeAmount ?? 0;
|
|
2026
|
+
/*
|
|
2027
|
+
* What the buyer pays TODAY on an instalment order — the token plus every
|
|
2028
|
+
* additive charge. Surfaced so the Order Summary can show both figures
|
|
2029
|
+
* rather than one number the buyer has to interpret.
|
|
2030
|
+
*/
|
|
2031
|
+
const groupEmiDeferred = groupEmiPreviewQuote?.remainingBalance ?? 0;
|
|
1607
2032
|
const groupGiftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissions);
|
|
1608
2033
|
/*
|
|
1609
2034
|
* C5e — the same three tiers `createOrderForGroup` enforces, so the figure
|
|
@@ -1617,6 +2042,28 @@ export async function previewCheckoutPricing(input) {
|
|
|
1617
2042
|
* seller cannot flip the switch after the sale).
|
|
1618
2043
|
*/
|
|
1619
2044
|
const groupHasFinalSale = group.some(({ product }) => product?.finalSale === true);
|
|
2045
|
+
/*
|
|
2046
|
+
* ── S5a in the PREVIEW ───────────────────────────────────────────────────
|
|
2047
|
+
*
|
|
2048
|
+
* 🛑 Here because a coordinator commits against this figure. The plan is
|
|
2049
|
+
* explicit that the fees "must be shown before the coordinator commits" —
|
|
2050
|
+
* a handling fee plus N−1 shipping legs discovered afterwards is exactly
|
|
2051
|
+
* the surprise the arrangement makes expensive.
|
|
2052
|
+
*
|
|
2053
|
+
* The same function and the same `countDestinations` as both order paths,
|
|
2054
|
+
* so the shown figure and the charged one cannot diverge. D37 is the record
|
|
2055
|
+
* of what ONE money term reaching three of four paths costs: the buyer was
|
|
2056
|
+
* shown one number, charged another, and the order recorded the first.
|
|
2057
|
+
*
|
|
2058
|
+
* 🛑 A preview NEVER throws, so an unpriced arrangement contributes 0 here
|
|
2059
|
+
* rather than raising — `quoteSharedPurchase` reports it on `unpriced` and
|
|
2060
|
+
* the ORDER path is what refuses. A preview that threw would render the
|
|
2061
|
+
* whole cart as an error page for a seller's missing config.
|
|
2062
|
+
*/
|
|
2063
|
+
const groupSharedPurchaseFee = quoteSharedPurchase({
|
|
2064
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
2065
|
+
rates: previewSharedRates,
|
|
2066
|
+
}).total;
|
|
1620
2067
|
const { shippingProtectionFee: groupShipmentProtectionFee, saleProtectionFee: groupSaleProtectionFee, } = computeProtectionFees({
|
|
1621
2068
|
subtotal: groupTotal,
|
|
1622
2069
|
selected: {
|
|
@@ -1681,6 +2128,7 @@ export async function previewCheckoutPricing(input) {
|
|
|
1681
2128
|
shippingFee: groupShippingFee,
|
|
1682
2129
|
codHandlingFee: groupCodHandlingFee,
|
|
1683
2130
|
emiSurcharge: groupEmiSurcharge,
|
|
2131
|
+
emiDeferred: groupEmiDeferred,
|
|
1684
2132
|
giftWrapFee: groupGiftWrapFee,
|
|
1685
2133
|
shipmentProtectionFee: groupShipmentProtectionFee,
|
|
1686
2134
|
saleProtectionFee: groupSaleProtectionFee,
|
|
@@ -1701,24 +2149,44 @@ export async function previewCheckoutPricing(input) {
|
|
|
1701
2149
|
groupEmiSurcharge +
|
|
1702
2150
|
groupGiftWrapFee +
|
|
1703
2151
|
groupShipmentProtectionFee +
|
|
1704
|
-
groupSaleProtectionFee
|
|
2152
|
+
groupSaleProtectionFee +
|
|
2153
|
+
// S5a — a term in the per-store total, so the seller card and the
|
|
2154
|
+
// order it previews agree.
|
|
2155
|
+
groupSharedPurchaseFee,
|
|
1705
2156
|
supportsShippingProtection: storeProtections.shipping,
|
|
1706
2157
|
supportsSaleProtection: storeProtections.sale,
|
|
1707
2158
|
hasFinalSaleItem: groupHasFinalSale,
|
|
2159
|
+
acceptsPoints,
|
|
2160
|
+
sharedPurchaseFee: groupSharedPurchaseFee,
|
|
2161
|
+
// Filled by the tender pass below — it needs each store's share of the
|
|
2162
|
+
// one capped commission, which is only allocatable once every group is
|
|
2163
|
+
// known.
|
|
2164
|
+
pointsApplied: 0,
|
|
1708
2165
|
});
|
|
1709
2166
|
}
|
|
1710
2167
|
const sum = (pick) => stores.reduce((acc, s) => acc + pick(s), 0);
|
|
1711
2168
|
const shippingFee = sum((s) => s.shippingFee);
|
|
1712
2169
|
const codHandlingFee = sum((s) => s.codHandlingFee);
|
|
1713
2170
|
const emiSurcharge = sum((s) => s.emiSurcharge);
|
|
2171
|
+
const emiDeferred = sum((s) => s.emiDeferred);
|
|
1714
2172
|
const giftWrapFeeTotal = sum((s) => s.giftWrapFee);
|
|
1715
2173
|
const shipmentProtectionFeeTotal = sum((s) => s.shipmentProtectionFee);
|
|
1716
2174
|
const saleProtectionFeeTotal = sum((s) => s.saleProtectionFee);
|
|
2175
|
+
// S5a — summed from the same per-store figures, so the aggregate cannot
|
|
2176
|
+
// disagree with the seller cards it is built from.
|
|
2177
|
+
const sharedPurchaseFeeTotal = sum((s) => s.sharedPurchaseFee);
|
|
1717
2178
|
const gstAmount = sum((s) => s.gstAmount);
|
|
1718
2179
|
const couponDiscount = sum((s) => s.couponDiscount);
|
|
1719
2180
|
// One capped commission for the whole checkout, on every payment method —
|
|
1720
2181
|
// deliberately not per store, so it is not added to the per-store rows above.
|
|
1721
|
-
|
|
2182
|
+
// The whole object is kept (not just the two fields) because the points
|
|
2183
|
+
// tender below re-allocates it per store through `allocateCheckoutFees`,
|
|
2184
|
+
// which takes a `CheckoutFees` — and `totalFee` carries the
|
|
2185
|
+
// `minimumTransactionFee` floor, which is exactly the field D41 found had no
|
|
2186
|
+
// consumer anywhere. Destructuring it away here would be how it loses its
|
|
2187
|
+
// second one.
|
|
2188
|
+
const checkoutFees = computeCheckoutFees(cartSubtotal, commissions);
|
|
2189
|
+
const { platformFee, gstOnFee } = checkoutFees;
|
|
1722
2190
|
/*
|
|
1723
2191
|
* 🛑 `gstAmount` is NOT a term, and leaving it in was a shown-vs-charged
|
|
1724
2192
|
* drift waiting on a settings flag.
|
|
@@ -1751,21 +2219,77 @@ export async function previewCheckoutPricing(input) {
|
|
|
1751
2219
|
giftWrapFeeTotal +
|
|
1752
2220
|
shipmentProtectionFeeTotal +
|
|
1753
2221
|
saleProtectionFeeTotal +
|
|
2222
|
+
// S5a — a real additive charge, so it belongs in the column with the other
|
|
2223
|
+
// fees rather than below the total like the tax-inclusive `gstAmount`.
|
|
2224
|
+
sharedPurchaseFeeTotal +
|
|
1754
2225
|
platformFee +
|
|
1755
2226
|
gstOnFee;
|
|
2227
|
+
/*
|
|
2228
|
+
* ── THE POINTS TENDER, PREVIEWED (D46/D47) ────────────────────────────────
|
|
2229
|
+
*
|
|
2230
|
+
* 🛑 Applied PER STORE against the SAME base the order path uses, which is
|
|
2231
|
+
* why this runs here and not inside the loop above: the base includes each
|
|
2232
|
+
* store's share of the one capped commission, and that share is only
|
|
2233
|
+
* allocatable once every group is known.
|
|
2234
|
+
*
|
|
2235
|
+
* 🛑 Per store is not a detail — it is the only shape that can agree with
|
|
2236
|
+
* what gets charged. A cart-level application gives the same answer as the
|
|
2237
|
+
* per-group one whenever every store accepts (the sum of per-group 50% caps
|
|
2238
|
+
* equals 50% of the sum), and a DIFFERENT answer the moment one store does
|
|
2239
|
+
* not: with ₹500 at an accepting store, ₹500 at a declining one and a 600
|
|
2240
|
+
* balance, per-store applies ₹250 and cart-level would promise ₹500. The
|
|
2241
|
+
* buyer would be shown a discount half of which could never be honoured.
|
|
2242
|
+
*
|
|
2243
|
+
* The iteration order matches `orderGroups`, and so does the order-creation
|
|
2244
|
+
* loop, so the budget is drawn down in the same sequence in both — which is
|
|
2245
|
+
* what makes the two sums identical rather than merely close.
|
|
2246
|
+
*/
|
|
2247
|
+
const previewUseAllPoints = cart.useAllPoints === true;
|
|
2248
|
+
const previewBalance = previewUseAllPoints ? await safePointsBalance(uid) : 0;
|
|
2249
|
+
if (previewUseAllPoints && previewBalance > 0) {
|
|
2250
|
+
const feeByStore = allocateCheckoutFees(stores.map((s) => [s.storeId, s.subtotal]), checkoutFees);
|
|
2251
|
+
const { perGroup } = allocatePointsAcrossGroups(stores.map((s) => {
|
|
2252
|
+
const share = feeByStore.get(s.storeId);
|
|
2253
|
+
return {
|
|
2254
|
+
// The SAME base `createOrderForGroup` applies the tender to: the
|
|
2255
|
+
// store's charged total plus its share of the one capped commission.
|
|
2256
|
+
// `store.total` deliberately excludes that share (it is a checkout
|
|
2257
|
+
// charge, not a per-store one), so it is added back here rather than
|
|
2258
|
+
// by widening a published field.
|
|
2259
|
+
total: roundRupees(s.total + (share?.platformFee ?? 0) + (share?.gstOnFee ?? 0)),
|
|
2260
|
+
accepted: s.acceptsPoints,
|
|
2261
|
+
};
|
|
2262
|
+
}), previewBalance, true);
|
|
2263
|
+
perGroup.forEach((applied, i) => {
|
|
2264
|
+
stores[i].pointsApplied = applied;
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
const pointsApplied = sum((s) => s.pointsApplied);
|
|
1756
2268
|
return {
|
|
1757
2269
|
subtotal: cartSubtotal,
|
|
1758
2270
|
shippingFee,
|
|
1759
2271
|
codHandlingFee,
|
|
1760
2272
|
emiSurcharge,
|
|
2273
|
+
emiDeferred,
|
|
1761
2274
|
giftWrapFee: giftWrapFeeTotal,
|
|
1762
2275
|
shipmentProtectionFee: shipmentProtectionFeeTotal,
|
|
1763
2276
|
saleProtectionFee: saleProtectionFeeTotal,
|
|
2277
|
+
sharedPurchaseFee: sharedPurchaseFeeTotal,
|
|
1764
2278
|
gstAmount,
|
|
1765
2279
|
couponDiscount,
|
|
1766
2280
|
platformFee,
|
|
1767
2281
|
gstOnFee,
|
|
1768
2282
|
total,
|
|
2283
|
+
pointsApplied,
|
|
2284
|
+
// Equal to `total` when no points apply, so every existing reader of
|
|
2285
|
+
// either figure keeps getting the right number.
|
|
2286
|
+
/*
|
|
2287
|
+
* 🛑 Both tenders, through the SAME helper the two order writers use —
|
|
2288
|
+
* so the figure the buyer is shown and the figure they are charged are
|
|
2289
|
+
* one expression, not two that agree today.
|
|
2290
|
+
*/
|
|
2291
|
+
amountDue: amountDueNow({ orderTotal: total, pointsApplied, emiRemainingBalance: emiDeferred }),
|
|
2292
|
+
pointsBalance: previewBalance,
|
|
1769
2293
|
stores,
|
|
1770
2294
|
};
|
|
1771
2295
|
}
|
|
@@ -1916,7 +2440,16 @@ async function refundDroppedItemsForPhonePeCheckout(input) {
|
|
|
1916
2440
|
* from the inline version this replaced.
|
|
1917
2441
|
*/
|
|
1918
2442
|
async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
1919
|
-
const { appliedCoupons, cartSubtotal, storeAddons, platformFeeByStore, siteSettings, uid, userName, userEmail, merchantOrderId, phonepeOrderId, transactionId, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, couponUsageAccumulator, productById: phonepeProductById, buyerState,
|
|
2443
|
+
const { appliedCoupons, cartSubtotal, storeAddons, platformFeeByStore, siteSettings, uid, userName, userEmail, merchantOrderId, phonepeOrderId, transactionId, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, couponUsageAccumulator, productById: phonepeProductById, buyerState, useAllPoints, pointsBudget,
|
|
2444
|
+
/*
|
|
2445
|
+
* 🛑 All three were DECLARED on the params above and never destructured,
|
|
2446
|
+
* so the body referenced them as free names: `emiOptIn`/`emiTenureMonths`
|
|
2447
|
+
* in the schedule call and `deliverySnapshot` in `placeOfSupply`. The
|
|
2448
|
+
* interface promised them and the function could not see them — which is
|
|
2449
|
+
* why appkit's own `tsc` is the gate for this file and not a reading of
|
|
2450
|
+
* the diff.
|
|
2451
|
+
*/
|
|
2452
|
+
emiOptIn, emiTenureMonths, deliverySnapshot, } = ctx;
|
|
1920
2453
|
/*
|
|
1921
2454
|
* Which online gateway actually took the money, read off the registered
|
|
1922
2455
|
* provider rather than passed in — exactly one is registered per process
|
|
@@ -1932,7 +2465,21 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
1932
2465
|
// seller group here, duplicated from resolveShippingCost.
|
|
1933
2466
|
// D44: `groupTotal` is computed just above — pass it so a free-above
|
|
1934
2467
|
// threshold can fire and an unpriceable store is recorded.
|
|
1935
|
-
const { shippingFee, storeOwnerId, storeState, storeTaxIdentity, storeProtections } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
2468
|
+
const { shippingFee, storeOwnerId, storeState, storeTaxIdentity, storeProtections, acceptsPoints, sharedPurchaseRates, storeEmiEnabled } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
2469
|
+
/*
|
|
2470
|
+
* The instalment schedule for THIS seller group, through the same helper the
|
|
2471
|
+
* COD/manual writer uses — not a copy of its block. Two hand-written
|
|
2472
|
+
* copies is the create-vs-create axis of Root Cause #39, and these two
|
|
2473
|
+
* functions have already drifted once over add-on fees (#65).
|
|
2474
|
+
*
|
|
2475
|
+
* 🛑 Gate FAILURES are silent here, unlike the manual path, and that is
|
|
2476
|
+
* deliberate rather than sloppy: the money is already captured by the time
|
|
2477
|
+
* this runs, so throwing would produce a charged-with-no-order (the D64
|
|
2478
|
+
* class). `/api/payment/create-order` applies the same gate BEFORE capture,
|
|
2479
|
+
* which is where a refusal belongs; if it declined EMI, it captured the full
|
|
2480
|
+
* total and no schedule is wanted here either.
|
|
2481
|
+
*/
|
|
2482
|
+
const phonepeEmiSchedule = groupEmiSchedule({ emiOptIn, emiTenureMonths, groupSubtotal: groupTotal, sellerEmiEnabled: storeEmiEnabled }, siteSettings?.emi);
|
|
1936
2483
|
/*
|
|
1937
2484
|
* Same supplier-of-record rule as `createOrderForGroup`, through the same
|
|
1938
2485
|
* function. This path (PhonePe verify / webhook) is the SECOND place an
|
|
@@ -2050,20 +2597,82 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2050
2597
|
/* Product GST is not a term — prices are tax-inclusive, so it is already
|
|
2051
2598
|
* inside `groupTotal`. Matches the COD/UPI path exactly; `platformFeeGst`
|
|
2052
2599
|
* stays because that is GST on our fee, charged on top. */
|
|
2600
|
+
/*
|
|
2601
|
+
* ── S5a: the shared-purchase arrangement, matching the COD/manual path ────
|
|
2602
|
+
*
|
|
2603
|
+
* Same function, same inputs, same position — before `orderTotal`, because
|
|
2604
|
+
* it is a term in it. See the fuller 🛑 at the COD/manual site for why the
|
|
2605
|
+
* rates are snapshotted and why an unpriced arrangement is refused rather
|
|
2606
|
+
* than quoted at ₹0.
|
|
2607
|
+
*
|
|
2608
|
+
* 🛑 On THIS path the money is already captured by the time this runs, so a
|
|
2609
|
+
* refusal here means a charged-with-no-order — the D64 class. That is why
|
|
2610
|
+
* `/api/payment/create-order` must apply the same term: the pre-payment
|
|
2611
|
+
* amount and this total have to agree, which is the parity C0c defect 1 was
|
|
2612
|
+
* about and the reason every addition here has to be made in three places.
|
|
2613
|
+
*/
|
|
2614
|
+
const phonepeSharedQuote = quoteSharedPurchase({
|
|
2615
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
2616
|
+
rates: sharedPurchaseRates,
|
|
2617
|
+
});
|
|
2053
2618
|
// D42: rounded once on the sum — see the COD/UPI path's 🛑 for why a raw
|
|
2054
2619
|
// float sum of seven 2dp terms reached Firestore as 1011.8000000000001.
|
|
2055
|
-
const orderTotal = roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst);
|
|
2620
|
+
const orderTotal = roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (phonepeEmiSchedule?.surchargeAmount ?? 0) + phonepeSharedQuote.total);
|
|
2621
|
+
/*
|
|
2622
|
+
* ── POINTS TENDER (D46/D47) — the same STAGE as the COD/manual path ───────
|
|
2623
|
+
*
|
|
2624
|
+
* Same four inputs, same function, same point in the sequence: immediately
|
|
2625
|
+
* after `orderTotal`'s single `roundRupees` and before anything is written.
|
|
2626
|
+
* Computing it here rather than in a shared builder is unavoidable — the two
|
|
2627
|
+
* paths derive `orderTotal` separately — but the ARITHMETIC is
|
|
2628
|
+
* `resolvePointsTender` in both, so they cannot disagree about the amount
|
|
2629
|
+
* the way the add-on fees once did (Root Cause #65).
|
|
2630
|
+
*
|
|
2631
|
+
* 🛑 This is the path whose capture matters most: `amountDue` is what the
|
|
2632
|
+
* Razorpay order is created for, so a divergence here is money taken that
|
|
2633
|
+
* the order does not record.
|
|
2634
|
+
*/
|
|
2635
|
+
const pointsOptedIn = useAllPoints === true;
|
|
2636
|
+
const pointsAccepted = pointsOptedIn && acceptsPoints;
|
|
2637
|
+
const { pointsApplied } = resolvePointsTender({
|
|
2638
|
+
orderTotal,
|
|
2639
|
+
balance: pointsAccepted ? pointsBudget.remaining : 0,
|
|
2640
|
+
accepted: pointsAccepted,
|
|
2641
|
+
optedIn: pointsOptedIn,
|
|
2642
|
+
});
|
|
2643
|
+
if (pointsApplied > 0)
|
|
2644
|
+
pointsBudget.remaining -= pointsApplied;
|
|
2645
|
+
/*
|
|
2646
|
+
* 🛑 `resolvePointsTender`'s own `amountDue` is DISCARDED here — it knows
|
|
2647
|
+
* only about points, and on an instalment order the deferred balance reduces
|
|
2648
|
+
* the collection too. `amountDueNow` composes both, and is the same
|
|
2649
|
+
* expression the manual writer and the pricing preview use, so the figure
|
|
2650
|
+
* recorded in `paymentRecord.amount` below cannot drift from the figure
|
|
2651
|
+
* `/api/payment/create-order` actually captured.
|
|
2652
|
+
*/
|
|
2653
|
+
const amountDue = amountDueNow({
|
|
2654
|
+
orderTotal,
|
|
2655
|
+
pointsApplied,
|
|
2656
|
+
emiRemainingBalance: phonepeEmiSchedule?.remainingBalance ?? 0,
|
|
2657
|
+
});
|
|
2056
2658
|
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
2057
2659
|
// Same expansion as the COD/UPI path. This was a THIRD hand-rolled copy of
|
|
2058
2660
|
// the price rule (`isBundle ? item.price : product.price`) that — like the
|
|
2059
2661
|
// two in the consumer's checkout route — silently omitted `lockedPrice`, so
|
|
2060
2662
|
// an accepted offer paid through PhonePe was RECORDED at the seller's list
|
|
2061
2663
|
// price. `lineTotalFor` inside the expansion is the single definition.
|
|
2062
|
-
const
|
|
2664
|
+
const basePhonepeItems = group.flatMap(({ item, product }) => {
|
|
2063
2665
|
const lt = (product?.listingType ?? "standard");
|
|
2064
2666
|
const itemRule = getListingRule(lt);
|
|
2065
2667
|
return expandCartLineToOrderRows(item, product, phonepeProductById).map((baseLine) => itemRule.decorateOrderItem(baseLine, product));
|
|
2066
2668
|
});
|
|
2669
|
+
/*
|
|
2670
|
+
* Part 14 — the parts freeze, on THIS path too. The comment four lines
|
|
2671
|
+
* above records the cost of the last thing added to one path and not the
|
|
2672
|
+
* other: an accepted offer paid through PhonePe was recorded at the
|
|
2673
|
+
* seller's list price.
|
|
2674
|
+
*/
|
|
2675
|
+
const orderItems = await freezePartsOnItems(basePhonepeItems);
|
|
2067
2676
|
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
2068
2677
|
const imageUrls = [
|
|
2069
2678
|
...new Set(group
|
|
@@ -2089,6 +2698,16 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2089
2698
|
quantity: totalQuantity,
|
|
2090
2699
|
unitPrice: group[0].product.price,
|
|
2091
2700
|
totalPrice: orderTotal,
|
|
2701
|
+
// D46/D47 — see the identical pair in `createOrderForGroup`. `totalPrice`
|
|
2702
|
+
// stays the full value of the supply; points reduce what is COLLECTED.
|
|
2703
|
+
pointsApplied: pointsApplied > 0 ? pointsApplied : undefined,
|
|
2704
|
+
/*
|
|
2705
|
+
* Written whenever it differs from the total — points OR instalments. It
|
|
2706
|
+
* was `pointsApplied > 0 ? … : undefined`, so an EMI order carried no
|
|
2707
|
+
* `amountDue` and every reader fell back to the full total.
|
|
2708
|
+
*/
|
|
2709
|
+
amountDue: amountDue !== orderTotal ? amountDue : undefined,
|
|
2710
|
+
...emiOrderFields(phonepeEmiSchedule, emiTenureMonths),
|
|
2092
2711
|
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
2093
2712
|
storeId: firstItem.storeId || undefined,
|
|
2094
2713
|
storeName: firstItem.storeName || undefined,
|
|
@@ -2120,7 +2739,15 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2120
2739
|
paymentRecord: {
|
|
2121
2740
|
method: gatewayName === "razorpay" ? "razorpay" : "phonepe",
|
|
2122
2741
|
transactionId: transactionId ?? merchantOrderId,
|
|
2123
|
-
|
|
2742
|
+
/*
|
|
2743
|
+
* 🛑 What the gateway actually CAPTURED, which is `amountDue` once points
|
|
2744
|
+
* are applied — not the order's value. This record is the evidence a
|
|
2745
|
+
* reconciler compares against the gateway's own figure (Phase 4 R2), so
|
|
2746
|
+
* writing `orderTotal` here would open an `amount_mismatch` case on every
|
|
2747
|
+
* points-funded order: the discount would read as money we failed to
|
|
2748
|
+
* collect.
|
|
2749
|
+
*/
|
|
2750
|
+
amount: amountDue,
|
|
2124
2751
|
paidAt: new Date(),
|
|
2125
2752
|
verifiedBy: `${gatewayName}-order-status`,
|
|
2126
2753
|
verificationMethod: "order-status",
|
|
@@ -2148,6 +2775,14 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2148
2775
|
// Recorded as well as charged — an order that collects a protection fee
|
|
2149
2776
|
// without a record of it cannot be reconciled against a later claim.
|
|
2150
2777
|
saleProtectionFee: saleProtectionFee > 0 ? saleProtectionFee : undefined,
|
|
2778
|
+
// S5a — snapshotted, matching the COD/manual path field for field.
|
|
2779
|
+
sharedPurchase: phonepeSharedQuote.isShared
|
|
2780
|
+
? {
|
|
2781
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
2782
|
+
handlingFee: phonepeSharedQuote.handlingFee,
|
|
2783
|
+
extraShippingFee: phonepeSharedQuote.extraShippingFee,
|
|
2784
|
+
}
|
|
2785
|
+
: undefined,
|
|
2151
2786
|
/*
|
|
2152
2787
|
* The tax breakdown, matching `createOrderForGroup` field for field.
|
|
2153
2788
|
* This path recorded NONE of it before C0c defect 1 — so an online order
|
|
@@ -2158,6 +2793,15 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2158
2793
|
*/
|
|
2159
2794
|
taxableAmount: phonepeGst?.taxableAmount,
|
|
2160
2795
|
exemptAmount: phonepeGst?.exemptAmount || undefined,
|
|
2796
|
+
// Phase 7 — the place of supply, matching `createOrderForGroup` field for
|
|
2797
|
+
// field. This path recorded NONE of the tax breakdown until C0c defect 1,
|
|
2798
|
+
// which is precisely why every addition here has to be made twice.
|
|
2799
|
+
/*
|
|
2800
|
+
* Part 14 — the snapshot's state is authoritative, because it is the
|
|
2801
|
+
* field the invoice and GSTR-1 actually read. `buyerState` stays as
|
|
2802
|
+
* the fallback for a digital cart, which has no delivery address.
|
|
2803
|
+
*/
|
|
2804
|
+
placeOfSupply: deliverySnapshot?.deliveryState || buyerState || undefined,
|
|
2161
2805
|
// Our registration at the moment of supply — see the schema. Mirrors the
|
|
2162
2806
|
// COD/UPI path so both produce an invoiceable order.
|
|
2163
2807
|
supplierGstin: supplier.gstin,
|
|
@@ -2177,6 +2821,9 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2177
2821
|
...extraOrderFields,
|
|
2178
2822
|
});
|
|
2179
2823
|
orderIds.push(order.id);
|
|
2824
|
+
// The second half of the points tender — one implementation, shared with the
|
|
2825
|
+
// COD/manual path. See `settlePointsTender`.
|
|
2826
|
+
await settlePointsTender({ orderId: order.id, userId: uid, pointsApplied, pointsBudget });
|
|
2180
2827
|
// Close the loop on any locked line this order settled: flip the offer to
|
|
2181
2828
|
// "paid" (its terminal status had NO server-side writer before 2026-08-21,
|
|
2182
2829
|
// so an accepted offer could be re-added and re-ordered) and back-link the
|
|
@@ -2426,6 +3073,7 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2426
3073
|
await assertLockedLinesStillValid(settleItems, uid);
|
|
2427
3074
|
const isDigitalCartPp = cartIsDigitalOnly(settleItems);
|
|
2428
3075
|
let shippingAddress;
|
|
3076
|
+
let deliverySnapshot;
|
|
2429
3077
|
let resolvedAddressPp = null;
|
|
2430
3078
|
if (!isDigitalCartPp) {
|
|
2431
3079
|
if (!addressId) {
|
|
@@ -2440,6 +3088,13 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2440
3088
|
throw new NotFoundError(ERROR_MESSAGES.CHECKOUT.ADDRESS_REQUIRED);
|
|
2441
3089
|
}
|
|
2442
3090
|
shippingAddress = formatShippingAddress(resolvedAddressPp);
|
|
3091
|
+
/*
|
|
3092
|
+
* 🛑 REVEALED first. `findById` returns this PII encrypted, and
|
|
3093
|
+
* `snapshotDeliveryAddress` demands `AddressPlain` precisely so that
|
|
3094
|
+
* freezing `enc:v1:…` onto an invoice is a compile error rather than
|
|
3095
|
+
* something discovered on a printed label.
|
|
3096
|
+
*/
|
|
3097
|
+
deliverySnapshot = snapshotDeliveryAddress(unitOfWork.addresses.revealAddress(resolvedAddressPp));
|
|
2443
3098
|
}
|
|
2444
3099
|
// SB-UNI-5 2026-05-13 — bundle-aware product fetch + validation. Each
|
|
2445
3100
|
// cart line gets paired with a "representative" product (first member id
|
|
@@ -2538,7 +3193,12 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2538
3193
|
// percentages, so the moment the cap landed it would have expected more
|
|
2539
3194
|
// than was actually collected and rejected every legitimate payment.
|
|
2540
3195
|
const commissionRates = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
2541
|
-
|
|
3196
|
+
// The whole object is kept so the points tender below can re-allocate it
|
|
3197
|
+
// per group — see the same note in `previewCheckoutPricing`. `totalFee`
|
|
3198
|
+
// carries the `minimumTransactionFee` floor, and destructuring it away is
|
|
3199
|
+
// how it lost every consumer once already (D41).
|
|
3200
|
+
const expectedFees = computeCheckoutFees(cartSubtotalRs, commissionRates);
|
|
3201
|
+
const { platformFee: expectedPlatformFee, gstOnFee: expectedGstOnFee } = expectedFees;
|
|
2542
3202
|
// Same per-seller-group shipping sum /api/payment/create-order charged
|
|
2543
3203
|
// upfront (and the same resolveShippingCost createPhonePeGroupOrder
|
|
2544
3204
|
// below will use to build each order's recorded totalPrice) — keeps this
|
|
@@ -2584,6 +3244,36 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2584
3244
|
goodsValue: g.items.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0),
|
|
2585
3245
|
})));
|
|
2586
3246
|
const expectedShippingFee = expectedShippingFees.reduce((sum, r) => sum + r.shippingFee, 0);
|
|
3247
|
+
/*
|
|
3248
|
+
* ── The instalment terms, which this guard must expect BOTH of ──────────
|
|
3249
|
+
*
|
|
3250
|
+
* 🛑 Two terms, pulling in opposite directions, and omitting either one
|
|
3251
|
+
* breaks the guard in the direction that costs the buyer an order:
|
|
3252
|
+
*
|
|
3253
|
+
* + surcharge — the cost of credit, a real addition to the total
|
|
3254
|
+
* − deferred — the financed principal plus that surcharge, which is NOT
|
|
3255
|
+
* collected today
|
|
3256
|
+
*
|
|
3257
|
+
* With neither term the guard would expect the un-surcharged FULL total
|
|
3258
|
+
* while the gateway captured only the token, trip `paid < expected − 1`,
|
|
3259
|
+
* and throw **after the money was taken** — a charged-with-no-order, the
|
|
3260
|
+
* D64 class, on every single EMI order. That is strictly worse than the
|
|
3261
|
+
* overcharge this guard exists to catch, which is the same trap D44's note
|
|
3262
|
+
* above records one defect over.
|
|
3263
|
+
*
|
|
3264
|
+
* Built from the same per-group goods values and the same `groupEmiQuote`
|
|
3265
|
+
* the capture route and the order writer call, so the only way the three
|
|
3266
|
+
* can disagree is a change made to one and not the others.
|
|
3267
|
+
*/
|
|
3268
|
+
const guardEmiPlanTenure = cart.emiPlan?.tenureMonths;
|
|
3269
|
+
const expectedEmiQuotes = expectedShippingGroups.map((g, i) => groupEmiQuote({
|
|
3270
|
+
emiOptIn: guardEmiPlanTenure !== undefined,
|
|
3271
|
+
emiTenureMonths: guardEmiPlanTenure,
|
|
3272
|
+
groupSubtotal: g.items.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0),
|
|
3273
|
+
sellerEmiEnabled: expectedShippingFees[i]?.storeEmiEnabled === true,
|
|
3274
|
+
}, siteSettings?.emi));
|
|
3275
|
+
const expectedEmiSurcharge = expectedEmiQuotes.reduce((sum, q) => sum + (q?.surchargeAmount ?? 0), 0);
|
|
3276
|
+
const expectedEmiDeferred = expectedEmiQuotes.reduce((sum, q) => sum + (q?.remainingBalance ?? 0), 0);
|
|
2587
3277
|
// Add-ons are per store, so the expected total sums each group's own
|
|
2588
3278
|
// selection — a single cart-wide figure would over- or under-count as soon
|
|
2589
3279
|
// as the buyer picked add-ons for only some sellers.
|
|
@@ -2624,11 +3314,70 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2624
3314
|
* would make this guard demand ~₹580 more than the buyer owes on a
|
|
2625
3315
|
* ₹5,547 cart — rejecting every correctly-charged order.
|
|
2626
3316
|
*/
|
|
2627
|
-
|
|
3317
|
+
/*
|
|
3318
|
+
* ── D46/D47 — the guard must expect the amount NET OF POINTS ─────────────
|
|
3319
|
+
*
|
|
3320
|
+
* 🛑 Omitting this term would make the guard reject every correctly-charged
|
|
3321
|
+
* points order, which is worse than the overcharge it exists to catch: the
|
|
3322
|
+
* money is already captured by the time this runs, so a false rejection is
|
|
3323
|
+
* a charged-with-no-order — the D64 class.
|
|
3324
|
+
*
|
|
3325
|
+
* The bases are rebuilt here from the same terms in the same order, and the
|
|
3326
|
+
* draw-down is the same shared allocator, so the only way this can diverge
|
|
3327
|
+
* from the capture route is a change made to one and not the other. That is
|
|
3328
|
+
* precisely the drift the four-way export comment on
|
|
3329
|
+
* `allocatePointsAcrossGroups` exists to make visible.
|
|
3330
|
+
*/
|
|
3331
|
+
const guardUseAllPoints = cart.useAllPoints === true;
|
|
3332
|
+
let expectedPointsApplied = 0;
|
|
3333
|
+
if (guardUseAllPoints) {
|
|
3334
|
+
const guardBalance = await safePointsBalance(uid);
|
|
3335
|
+
if (guardBalance > 0) {
|
|
3336
|
+
const guardSubtotals = expectedShippingGroups.map((g) => [
|
|
3337
|
+
g.items[0].item.storeId,
|
|
3338
|
+
g.items.reduce((gs, { item, product }) => gs + lineTotalFor(item, product), 0),
|
|
3339
|
+
]);
|
|
3340
|
+
const guardFeeByGroup = allocateCheckoutFees(guardSubtotals, expectedFees);
|
|
3341
|
+
const guardCouponByGroup = computeCartCouponDiscount(cart.appliedCoupons ?? [], expectedShippingGroups.map((g) => ({
|
|
3342
|
+
storeId: g.items[0].item.storeId,
|
|
3343
|
+
lines: g.items.map(({ item, product }) => ({
|
|
3344
|
+
itemId: item.itemId,
|
|
3345
|
+
lineTotal: lineTotalFor(item, product),
|
|
3346
|
+
})),
|
|
3347
|
+
}))).perGroup;
|
|
3348
|
+
/*
|
|
3349
|
+
* The per-group add-on fee, lifted out of the `map` below so that
|
|
3350
|
+
* callback has no block body. Its statements sat at brace-depth 6
|
|
3351
|
+
* inside `if → if → callback`, which DEEP_NESTING blocks — and the
|
|
3352
|
+
* add-on sum is the one part of this base the sibling money path
|
|
3353
|
+
* (`POST /api/payment/create-order`) computes earlier rather than
|
|
3354
|
+
* inline, so it is also the natural seam.
|
|
3355
|
+
*/
|
|
3356
|
+
const guardAddonFee = (storeId, goods) => {
|
|
3357
|
+
const addons = cart.storeAddons?.[storeId] ?? {};
|
|
3358
|
+
return (computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates) +
|
|
3359
|
+
computeShipmentProtectionFee(goods, addons.shipmentProtectionAddon ?? false, commissionRates));
|
|
3360
|
+
};
|
|
3361
|
+
const guardBases = expectedShippingGroups.map((g, i) => pointsBaseForGroup({
|
|
3362
|
+
goods: guardSubtotals[i][1],
|
|
3363
|
+
couponDiscount: guardCouponByGroup[i]?.couponDiscount ?? 0,
|
|
3364
|
+
shippingFee: expectedShippingFees[i]?.shippingFee ?? 0,
|
|
3365
|
+
addonFee: guardAddonFee(g.items[0].item.storeId, guardSubtotals[i][1]),
|
|
3366
|
+
platformFee: guardFeeByGroup.get(g.items[0].item.storeId)?.platformFee ?? 0,
|
|
3367
|
+
gstOnFee: guardFeeByGroup.get(g.items[0].item.storeId)?.gstOnFee ?? 0,
|
|
3368
|
+
acceptsPoints: expectedShippingFees[i]?.acceptsPoints === true,
|
|
3369
|
+
}));
|
|
3370
|
+
expectedPointsApplied = allocatePointsAcrossGroups(guardBases, guardBalance, true).pointsApplied;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
const expectedPaymentAmountRs = roundRupees(Math.max(0, expectedDiscountedSubtotal +
|
|
2628
3374
|
expectedPlatformFee +
|
|
2629
3375
|
expectedGstOnFee +
|
|
2630
3376
|
expectedAddonFees +
|
|
2631
|
-
expectedShippingFee
|
|
3377
|
+
expectedShippingFee +
|
|
3378
|
+
expectedEmiSurcharge -
|
|
3379
|
+
expectedEmiDeferred -
|
|
3380
|
+
expectedPointsApplied));
|
|
2632
3381
|
// Reuses the order already fetched above to confirm COMPLETED — PhonePe's
|
|
2633
3382
|
// own amount is in its wire-format smallest unit. // audit-money-units-ok: describes the PhonePe boundary conversion this line performs, not our storage convention
|
|
2634
3383
|
const paidAmountRs = paiseToRupees(gatewayOrder.amount);
|
|
@@ -2703,6 +3452,19 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2703
3452
|
g[0].item.storeId,
|
|
2704
3453
|
g.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0),
|
|
2705
3454
|
]), computeCheckoutFees(cartSubtotal, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS));
|
|
3455
|
+
/*
|
|
3456
|
+
* The points budget for this checkout — one read, zero when not opted in.
|
|
3457
|
+
* See the identical pair in the COD/manual path for why it is a shared
|
|
3458
|
+
* remainder rather than a per-group balance read.
|
|
3459
|
+
*
|
|
3460
|
+
* 🛑 On THIS path the money has already been captured by the time these
|
|
3461
|
+
* orders are created, so the budget must match whatever the pre-payment
|
|
3462
|
+
* amount was computed against. That is why `useAllPoints` is read off the
|
|
3463
|
+
* cart here too and not re-decided: the figure quoted to the gateway and the
|
|
3464
|
+
* figure recorded on the orders come from the same cart flag.
|
|
3465
|
+
*/
|
|
3466
|
+
const useAllPoints = cart.useAllPoints === true;
|
|
3467
|
+
const pointsBudget = { remaining: useAllPoints ? await safePointsBalance(uid) : 0 };
|
|
2706
3468
|
for (const { items: group, orderType } of orderGroups) {
|
|
2707
3469
|
total += await createPhonePeGroupOrder(group, orderType, {
|
|
2708
3470
|
appliedCoupons,
|
|
@@ -2728,6 +3490,15 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2728
3490
|
emailsToSend,
|
|
2729
3491
|
couponUsageAccumulator,
|
|
2730
3492
|
productById: productByIdPaid,
|
|
3493
|
+
useAllPoints,
|
|
3494
|
+
pointsBudget,
|
|
3495
|
+
/*
|
|
3496
|
+
* Resolved from the CART, exactly as the capture route did — so the
|
|
3497
|
+
* schedule written onto the order is built from the same tenure the
|
|
3498
|
+
* gateway was asked to capture against.
|
|
3499
|
+
*/
|
|
3500
|
+
emiOptIn: cart.emiPlan?.tenureMonths !== undefined,
|
|
3501
|
+
emiTenureMonths: cart.emiPlan?.tenureMonths,
|
|
2731
3502
|
});
|
|
2732
3503
|
}
|
|
2733
3504
|
await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
|