@mohasinac/appkit 4.59.0 → 4.60.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 +62 -5
- package/dist/_internal/client/features/maintenance/views/MaintenanceDashboardView.js +2 -2
- package/dist/_internal/server/features/brands/actions.js +78 -4
- package/dist/_internal/server/features/checkout/actions.d.ts +36 -13
- package/dist/_internal/server/features/checkout/actions.js +668 -183
- package/dist/_internal/server/features/checkout/supplier-identity.js +18 -1
- package/dist/_internal/server/features/gateway-events/index.d.ts +10 -0
- package/dist/_internal/server/features/gateway-events/index.js +10 -0
- package/dist/_internal/server/features/gateway-events/ingest.d.ts +127 -0
- package/dist/_internal/server/features/gateway-events/ingest.js +188 -0
- package/dist/_internal/server/features/gateway-events/redact.d.ts +66 -0
- package/dist/_internal/server/features/gateway-events/redact.js +151 -0
- package/dist/_internal/server/features/grievance/actions.d.ts +63 -0
- package/dist/_internal/server/features/grievance/actions.js +173 -0
- package/dist/_internal/server/features/grievance/index.d.ts +8 -0
- package/dist/_internal/server/features/grievance/index.js +8 -0
- package/dist/_internal/server/features/grouped/data.js +28 -3
- package/dist/_internal/server/features/lottery/actions.js +99 -4
- package/dist/_internal/server/features/order-completion/actions.d.ts +92 -0
- package/dist/_internal/server/features/order-completion/actions.js +336 -0
- package/dist/_internal/server/features/order-completion/index.d.ts +7 -0
- package/dist/_internal/server/features/order-completion/index.js +7 -0
- package/dist/_internal/server/features/orders/actions.d.ts +13 -0
- package/dist/_internal/server/features/orders/actions.js +56 -2
- package/dist/_internal/server/features/orders/adapters.js +26 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +116 -0
- package/dist/_internal/server/features/orders/cancellation-refund.js +466 -0
- package/dist/_internal/server/features/orders/index.d.ts +2 -1
- package/dist/_internal/server/features/orders/index.js +1 -1
- package/dist/_internal/server/features/orders/service.d.ts +60 -0
- package/dist/_internal/server/features/orders/service.js +74 -0
- package/dist/_internal/server/features/payment-cases/disputes.d.ts +76 -0
- package/dist/_internal/server/features/payment-cases/disputes.js +241 -0
- package/dist/_internal/server/features/payment-cases/index.d.ts +9 -0
- package/dist/_internal/server/features/payment-cases/index.js +9 -0
- package/dist/_internal/server/features/payouts/actions.d.ts +28 -3
- package/dist/_internal/server/features/payouts/actions.js +84 -10
- package/dist/_internal/server/features/points/adjust.d.ts +61 -0
- package/dist/_internal/server/features/points/adjust.js +121 -0
- package/dist/_internal/server/features/points/award.d.ts +65 -0
- package/dist/_internal/server/features/points/award.js +145 -0
- package/dist/_internal/server/features/price-index/market.d.ts +33 -0
- package/dist/_internal/server/features/price-index/market.js +54 -1
- package/dist/_internal/server/features/products/data.js +40 -22
- package/dist/_internal/server/features/protection-claims/actions.d.ts +72 -0
- package/dist/_internal/server/features/protection-claims/actions.js +228 -0
- package/dist/_internal/server/features/protection-claims/index.d.ts +8 -0
- package/dist/_internal/server/features/protection-claims/index.js +8 -0
- package/dist/_internal/server/features/refunds/actions.d.ts +20 -0
- package/dist/_internal/server/features/refunds/actions.js +259 -17
- package/dist/_internal/server/features/refunds/capacity.d.ts +29 -0
- package/dist/_internal/server/features/refunds/capacity.js +37 -0
- package/dist/_internal/server/features/reviews/actions.js +3 -2
- package/dist/_internal/server/features/search/index-build/sources.js +20 -1
- package/dist/_internal/server/features/shipping/reconcile.d.ts +10 -1
- package/dist/_internal/server/features/shipping/reconcile.js +34 -3
- package/dist/_internal/server/features/shipping/reimbursement.d.ts +117 -0
- package/dist/_internal/server/features/shipping/reimbursement.js +223 -0
- package/dist/_internal/server/features/stores/tabs.d.ts +19 -4
- package/dist/_internal/server/features/stores/tabs.js +24 -4
- package/dist/_internal/server/functions/https.d.ts +31 -1
- package/dist/_internal/server/functions/https.js +52 -1
- package/dist/_internal/server/functions/scheduled.d.ts +72 -1
- package/dist/_internal/server/functions/scheduled.js +124 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +62 -16
- package/dist/_internal/server/jobs/core/boxRollupReconcile.d.ts +46 -0
- package/dist/_internal/server/jobs/core/boxRollupReconcile.js +142 -0
- package/dist/_internal/server/jobs/core/dailyDataCleanup.js +149 -1
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +14 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +79 -1
- package/dist/_internal/server/jobs/core/index.d.ts +2 -0
- package/dist/_internal/server/jobs/core/index.js +2 -0
- package/dist/_internal/server/jobs/core/marketPriceRollup.d.ts +64 -0
- package/dist/_internal/server/jobs/core/marketPriceRollup.js +153 -0
- package/dist/_internal/server/jobs/core/obligationSweep.d.ts +36 -0
- package/dist/_internal/server/jobs/core/obligationSweep.js +503 -0
- package/dist/_internal/server/jobs/core/paymentReconcile.d.ts +79 -0
- package/dist/_internal/server/jobs/core/paymentReconcile.js +614 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +34 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +35 -4
- package/dist/_internal/server/jobs/handlers/boxRollupReconcile.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/boxRollupReconcile.js +15 -0
- package/dist/_internal/server/jobs/handlers/countersReconcileNow.d.ts +65 -0
- package/dist/_internal/server/jobs/handlers/countersReconcileNow.js +89 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +5 -0
- package/dist/_internal/server/jobs/handlers/index.js +5 -0
- package/dist/_internal/server/jobs/handlers/marketPriceRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/marketPriceRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/obligationSweep.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/obligationSweep.js +15 -0
- package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +30 -1
- package/dist/_internal/server/jobs/handlers/paymentReconcile.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/paymentReconcile.js +6 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +71 -0
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +73 -0
- package/dist/_internal/shared/checkout/coupon-discount.js +103 -0
- package/dist/_internal/shared/checkout/lanes.d.ts +42 -0
- package/dist/_internal/shared/checkout/lanes.js +46 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +149 -1
- package/dist/_internal/shared/checkout/order-math.js +251 -3
- package/dist/_internal/shared/checkout/refundable-base.d.ts +116 -0
- package/dist/_internal/shared/checkout/refundable-base.js +122 -0
- package/dist/_internal/shared/features/auctions/config.d.ts +14 -1
- package/dist/_internal/shared/features/auctions/config.js +30 -1
- package/dist/_internal/shared/features/brands/schema.d.ts +42 -0
- package/dist/_internal/shared/features/brands/schema.js +18 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +22 -0
- package/dist/_internal/shared/features/orders/refund-copy.js +34 -0
- package/dist/_internal/shared/features/print/label-profiles.d.ts +198 -0
- package/dist/_internal/shared/features/print/label-profiles.js +350 -0
- package/dist/_internal/shared/features/products/to-product-item.js +33 -5
- package/dist/_internal/shared/fees/calculator.d.ts +31 -2
- package/dist/_internal/shared/fees/calculator.js +103 -5
- package/dist/_internal/shared/fees/india-states.d.ts +195 -0
- package/dist/_internal/shared/fees/india-states.js +190 -0
- package/dist/_internal/shared/fees/payout-hold.d.ts +80 -0
- package/dist/_internal/shared/fees/payout-hold.js +74 -0
- package/dist/_internal/shared/obligations/deadlines.d.ts +94 -0
- package/dist/_internal/shared/obligations/deadlines.js +155 -0
- package/dist/_internal/shared/shipping/quote.d.ts +96 -0
- package/dist/_internal/shared/shipping/quote.js +67 -0
- package/dist/_internal/shared/time/business-days.d.ts +91 -0
- package/dist/_internal/shared/time/business-days.js +185 -0
- package/dist/cli/index.js +0 -2
- package/dist/client.d.ts +9 -4
- package/dist/client.js +26 -3
- package/dist/configs/tailwind.d.ts +0 -12
- package/dist/configs/tailwind.js +28 -15
- package/dist/constants/api-endpoints.d.ts +51 -9
- package/dist/constants/api-endpoints.js +23 -7
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.js +1 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/payment.d.ts +131 -0
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/about/components/DeveloperView.js +5 -2
- package/dist/features/about/components/FAQPageView.js +2 -1
- package/dist/features/about/components/FeesView.js +2 -1
- package/dist/features/about/components/HelpPageView.js +2 -1
- package/dist/features/about/components/HowAuctionsWorkView.js +2 -1
- package/dist/features/about/components/HowCheckoutWorksView.js +2 -1
- package/dist/features/about/components/HowEmiWorksView.js +2 -1
- package/dist/features/about/components/HowOffersWorkView.js +2 -1
- package/dist/features/about/components/HowOrdersWorkView.js +2 -1
- package/dist/features/about/components/HowPayoutsWorkView.js +2 -1
- package/dist/features/about/components/HowPreOrdersWorkView.js +2 -1
- package/dist/features/about/components/HowReviewsWorkView.js +2 -1
- package/dist/features/about/components/PolicyPageView.js +2 -1
- package/dist/features/about/components/ProseHero.d.ts +73 -0
- package/dist/features/about/components/ProseHero.js +18 -0
- package/dist/features/about/components/SecurityPrivacyView.js +2 -1
- package/dist/features/about/components/ShippingPolicyView.js +2 -1
- package/dist/features/about/components/TrackOrderView.js +2 -1
- package/dist/features/about/components/UnauthorizedView.js +1 -1
- package/dist/features/about/index.d.ts +2 -0
- package/dist/features/about/index.js +5 -0
- package/dist/features/account/actions/address-actions.d.ts +26 -2
- package/dist/features/account/actions/address-actions.js +25 -1
- package/dist/features/account/components/BuyerShoppingGuideView.js +1 -1
- package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
- package/dist/features/account/components/UserOrdersView.js +17 -0
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/account/hooks/useNotifications.js +29 -0
- package/dist/features/admin/actions/admin-coupon-actions.js +36 -0
- package/dist/features/admin/components/AdminBeysView.js +2 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
- package/dist/features/admin/components/AdminBrandEditorView.js +32 -2
- package/dist/features/admin/components/AdminBundleEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCategoriesView.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +73 -17
- package/dist/features/admin/components/AdminContactEditorView.js +1 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +26 -1
- package/dist/features/admin/components/AdminFaqEditorView.js +1 -1
- package/dist/features/admin/components/AdminFaqsView.js +2 -2
- package/dist/features/admin/components/AdminGatewayEventsView.d.ts +54 -0
- package/dist/features/admin/components/AdminGatewayEventsView.js +209 -0
- package/dist/features/admin/components/AdminGuideHubView.js +1 -1
- package/dist/features/admin/components/AdminMoneyCasesView.d.ts +60 -0
- package/dist/features/admin/components/AdminMoneyCasesView.js +287 -0
- package/dist/features/admin/components/AdminPartsView.js +2 -2
- package/dist/features/admin/components/AdminProductsView.js +75 -4
- package/dist/features/admin/components/AdminSidebar.js +2 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +65 -7
- package/dist/features/admin/components/AdminSublistingCategoriesView.js +2 -0
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +1 -1
- package/dist/features/admin/components/AdminSupportTicketsView.js +2 -2
- package/dist/features/admin/components/AdminTesterChecklistView.js +2 -2
- package/dist/features/admin/components/AdminUsersView.js +72 -1
- package/dist/features/admin/components/AdminViewCards.d.ts +3 -1
- package/dist/features/admin/components/AdminViewCards.js +4 -4
- package/dist/features/admin/components/DataListingView.d.ts +28 -0
- package/dist/features/admin/components/DataListingView.js +175 -14
- package/dist/features/admin/components/DataTable.d.ts +12 -1
- package/dist/features/admin/components/DataTable.js +3 -3
- package/dist/features/admin/components/OwnerQuickActions.d.ts +1 -1
- package/dist/features/admin/components/OwnerQuickActions.js +60 -2
- package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +1 -1
- package/dist/features/admin/components/RecordOwnerQuickActions.js +61 -14
- package/dist/features/admin/components/analytics/AdminStatCard.d.ts +35 -1
- package/dist/features/admin/components/analytics/AdminStatCard.js +77 -2
- package/dist/features/admin/components/index.d.ts +4 -0
- package/dist/features/admin/components/index.js +6 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +156 -0
- package/dist/features/admin/constants/filter-tabs.js +106 -0
- package/dist/features/admin/repository/site-settings.repository.d.ts +22 -0
- package/dist/features/admin/repository/site-settings.repository.js +82 -1
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +21 -0
- package/dist/features/admin/schemas/admin-editor-forms.js +44 -0
- package/dist/features/admin/schemas/firestore.js +5 -1
- package/dist/features/admin/schemas/site-settings-form.js +30 -1
- package/dist/features/auctions/actions/bid-actions.js +13 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +2 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +3 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +5 -3
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +8 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +2 -2
- package/dist/features/audit-log/schemas/firestore.d.ts +28 -0
- package/dist/features/audit-log/schemas/firestore.js +14 -0
- package/dist/features/auth/permissions/constants.d.ts +17 -1
- package/dist/features/auth/permissions/constants.js +18 -0
- package/dist/features/blog/components/BlogPostView.js +1 -1
- package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +1 -1
- package/dist/features/categories/api/[id]/description-template/route.js +107 -1
- package/dist/features/categories/components/BrandDetailPageView.js +2 -2
- package/dist/features/categories/components/BreadcrumbTrail.js +7 -1
- package/dist/features/categories/components/BundleDetailView.js +1 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +2 -2
- package/dist/features/categories/components/CategoryForm.js +12 -1
- package/dist/features/categories/components/CategoryTemplatePicker.js +38 -5
- package/dist/features/categories/repository/market-price-index.repository.d.ts +123 -0
- package/dist/features/categories/repository/market-price-index.repository.js +105 -0
- package/dist/features/categories/repository/price-observation.repository.d.ts +166 -0
- package/dist/features/categories/repository/price-observation.repository.js +132 -0
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/events/components/EventStatusBadge.js +1 -1
- package/dist/features/faq/components/FAQAccordion.js +2 -1
- package/dist/features/faq/components/FAQCategorySidebar.js +2 -2
- package/dist/features/faq/components/FAQHelpfulButtons.js +4 -4
- package/dist/features/faq/components/RelatedFAQs.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/filters/SwitchFilter.js +1 -1
- package/dist/features/gateway-events/repository/gateway-events.repository.d.ts +117 -0
- package/dist/features/gateway-events/repository/gateway-events.repository.js +274 -0
- package/dist/features/gateway-events/schemas/firestore.d.ts +217 -0
- package/dist/features/gateway-events/schemas/firestore.js +125 -0
- package/dist/features/gateway-events/schemas/index.d.ts +81 -0
- package/dist/features/gateway-events/schemas/index.js +79 -0
- package/dist/features/grouped/components/GroupedListingDetailView.js +1 -1
- package/dist/features/homepage/components/AnnouncementBar.js +37 -1
- package/dist/features/homepage/components/AnnouncementBar.style.css +30 -0
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CTABannerSection.js +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +14 -24
- package/dist/features/homepage/components/HeroCarousel.js +2 -2
- package/dist/features/homepage/components/SocialPostCard.js +1 -1
- package/dist/features/inventory/components/AdminMoveLedgerView.d.ts +7 -0
- package/dist/features/inventory/components/AdminMoveLedgerView.js +157 -0
- package/dist/features/inventory/components/AdminStorageBoxesView.d.ts +11 -0
- package/dist/features/inventory/components/AdminStorageBoxesView.js +168 -0
- package/dist/features/inventory/components/ReceiveStationView.d.ts +39 -0
- package/dist/features/inventory/components/ReceiveStationView.js +413 -0
- package/dist/features/inventory/components/operator-message.d.ts +29 -0
- package/dist/features/inventory/components/operator-message.js +41 -0
- package/dist/features/inventory/index.d.ts +13 -0
- package/dist/features/inventory/index.js +28 -0
- package/dist/features/inventory/repository/inventory-movements.repository.d.ts +97 -0
- package/dist/features/inventory/repository/inventory-movements.repository.js +186 -0
- package/dist/features/inventory/repository/inventory-placements.repository.d.ts +179 -0
- package/dist/features/inventory/repository/inventory-placements.repository.js +345 -0
- package/dist/features/inventory/repository/storage-boxes.repository.d.ts +83 -0
- package/dist/features/inventory/repository/storage-boxes.repository.js +127 -0
- package/dist/features/inventory/schemas/firestore.d.ts +197 -0
- package/dist/features/inventory/schemas/firestore.js +159 -0
- package/dist/features/layout/AppLayoutShell.d.ts +12 -1
- package/dist/features/layout/AppLayoutShell.js +25 -12
- package/dist/features/layout/AutoBreadcrumbs.js +162 -3
- package/dist/features/layout/AutoBreadcrumbs.style.css +45 -0
- package/dist/features/layout/BottomNavbar.d.ts +1 -1
- package/dist/features/layout/BottomNavbar.js +2 -2
- package/dist/features/media/MediaAvatar.js +1 -1
- package/dist/features/media/MediaImage.d.ts +24 -4
- package/dist/features/media/MediaImage.js +44 -4
- package/dist/features/media/MediaLightbox.js +1 -1
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/media/upload/MediaUploadField.js +91 -3
- package/dist/features/obligations/constants/policies.d.ts +92 -0
- package/dist/features/obligations/constants/policies.js +378 -0
- package/dist/features/obligations/repository/obligations.repository.d.ts +68 -0
- package/dist/features/obligations/repository/obligations.repository.js +158 -0
- package/dist/features/obligations/schemas/firestore.d.ts +160 -0
- package/dist/features/obligations/schemas/firestore.js +87 -0
- package/dist/features/orders/actions/order-actions.d.ts +43 -4
- package/dist/features/orders/actions/order-actions.js +60 -3
- package/dist/features/orders/actions/refund-actions.js +70 -10
- package/dist/features/orders/components/OrderCompletionPanel.d.ts +27 -0
- package/dist/features/orders/components/OrderCompletionPanel.js +192 -0
- package/dist/features/orders/components/OrdersList.js +2 -2
- package/dist/features/orders/components/RefundHistoryTable.js +3 -1
- package/dist/features/orders/components/index.d.ts +2 -0
- package/dist/features/orders/components/index.js +6 -0
- package/dist/features/orders/constants/transitions.d.ts +59 -0
- package/dist/features/orders/constants/transitions.js +104 -0
- package/dist/features/orders/repository/orders.repository.d.ts +109 -6
- package/dist/features/orders/repository/orders.repository.js +464 -21
- package/dist/features/orders/schemas/firestore.d.ts +270 -0
- package/dist/features/orders/schemas/firestore.js +24 -0
- package/dist/features/orders/schemas/index.d.ts +61 -0
- package/dist/features/orders/schemas/index.js +22 -0
- package/dist/features/orders/types/index.d.ts +79 -0
- package/dist/features/parts/components/PartsIndexPageView.js +8 -1
- package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +170 -0
- package/dist/features/payment-cases/repository/payment-cases.repository.js +342 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +229 -0
- package/dist/features/payment-cases/schemas/firestore.js +105 -0
- package/dist/features/payment-cases/schemas/resolve-form.d.ts +97 -0
- package/dist/features/payment-cases/schemas/resolve-form.js +114 -0
- package/dist/features/payments/repository/payout.repository.d.ts +0 -1
- package/dist/features/payments/repository/payout.repository.js +0 -1
- package/dist/features/points/repository/points.repository.d.ts +122 -0
- package/dist/features/points/repository/points.repository.js +304 -1
- package/dist/features/points/schemas/adjust-form.d.ts +58 -0
- package/dist/features/points/schemas/adjust-form.js +63 -0
- package/dist/features/points/schemas/firestore.d.ts +108 -1
- package/dist/features/points/schemas/firestore.js +84 -0
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
- package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +23 -4
- package/dist/features/price-index/components/PriceIndexToolView.js +71 -6
- package/dist/features/price-index/market-stats.d.ts +82 -0
- package/dist/features/price-index/market-stats.js +230 -0
- package/dist/features/print/components/BoxLabelPrintView.d.ts +25 -0
- package/dist/features/print/components/BoxLabelPrintView.js +66 -0
- package/dist/features/print/components/LabelProfilePicker.d.ts +13 -0
- package/dist/features/print/components/LabelProfilePicker.js +38 -0
- package/dist/features/print/components/labels.d.ts +117 -0
- package/dist/features/print/components/labels.js +258 -0
- package/dist/features/print/components/labels.style.css +46 -0
- package/dist/features/print/index.d.ts +6 -0
- package/dist/features/print/index.js +16 -0
- package/dist/features/products/columns/productTableColumns.js +5 -4
- package/dist/features/products/components/MarketplaceBundleCard.js +2 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +6 -6
- package/dist/features/products/components/PrizeDrawCollage.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
- package/dist/features/products/components/PrizeRevealModal.js +2 -2
- package/dist/features/products/components/ProductDetailPageView.js +18 -4
- package/dist/features/products/components/ProductForm.js +34 -23
- package/dist/features/products/components/ProductGrid.js +86 -62
- package/dist/features/products/constants/card-badges.d.ts +97 -0
- package/dist/features/products/constants/card-badges.js +100 -0
- package/dist/features/products/repository/products.repository.js +34 -1
- package/dist/features/products/schemas/firestore.d.ts +46 -3
- package/dist/features/products/schemas/firestore.js +21 -0
- package/dist/features/promotions/components/PromotionsView.js +1 -1
- package/dist/features/promotions/repository/claimed-coupons.repository.d.ts +32 -0
- package/dist/features/promotions/repository/claimed-coupons.repository.js +58 -0
- package/dist/features/promotions/schemas/firestore.js +30 -3
- package/dist/features/promotions/schemas/index.d.ts +6 -6
- package/dist/features/protection-claims/repository/protection-claims.repository.d.ts +54 -0
- package/dist/features/protection-claims/repository/protection-claims.repository.js +174 -0
- package/dist/features/protection-claims/schemas/firestore.d.ts +168 -0
- package/dist/features/protection-claims/schemas/firestore.js +130 -0
- package/dist/features/reviews/actions/review-actions.js +3 -2
- package/dist/features/reviews/components/ReviewModal.js +1 -1
- package/dist/features/reviews/components/ReviewsList.js +1 -1
- package/dist/features/reviews/schemas/firestore.d.ts +20 -1
- package/dist/features/scams/constants/scam-types.d.ts +2 -0
- package/dist/features/scams/constants/scam-types.js +18 -0
- package/dist/features/scams/schemas/report-form.d.ts +3 -3
- package/dist/features/scams/schemas/report-form.js +26 -1
- package/dist/features/search/components/Search.js +9 -9
- package/dist/features/search/components/SearchResultsClient.js +18 -3
- package/dist/features/search/components/SearchResultsPageView.d.ts +1 -6
- package/dist/features/search/components/SearchResultsPageView.js +2 -2
- package/dist/features/seller/actions/offer-actions.js +16 -1
- package/dist/features/seller/actions/seller-actions.js +14 -3
- package/dist/features/seller/components/CategoryInlineSelect.js +75 -11
- package/dist/features/seller/components/PrintCenterView.d.ts +5 -0
- package/dist/features/seller/components/PrintCenterView.js +41 -30
- package/dist/features/seller/components/SellerOrdersView.js +1 -1
- package/dist/features/seller/components/SellerPayoutRequestView.js +16 -5
- package/dist/features/seller/components/SellerPayoutSettingsView.js +1 -1
- package/dist/features/seller/components/SellerProductShell.d.ts +16 -0
- package/dist/features/seller/components/SellerProductShell.js +123 -13
- package/dist/features/seller/components/SellerReviewsView.js +4 -2
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/seller/schemas/coupon-form.d.ts +26 -0
- package/dist/features/seller/schemas/coupon-form.js +27 -0
- package/dist/features/seller/schemas/product-form.d.ts +3 -0
- package/dist/features/seller/schemas/product-form.js +5 -0
- package/dist/features/shell/FormShell.js +2 -2
- package/dist/features/shell/QuickFormDrawer.js +14 -0
- package/dist/features/shell/SectionForm.d.ts +19 -0
- package/dist/features/shell/SectionForm.js +133 -9
- package/dist/features/shell/SectionForm.style.css +54 -0
- package/dist/features/shipments/components/AdminShipmentsView.js +16 -1
- package/dist/features/shipments/repository/shipment-items.repository.d.ts +9 -0
- package/dist/features/shipments/repository/shipment-items.repository.js +14 -0
- package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
- package/dist/features/sticker-trace/components/StickerCommitPanel.js +1 -1
- package/dist/features/store-extensions/schemas/report-review-form.d.ts +8 -8
- package/dist/features/stores/components/StoreGuideHubView.js +1 -1
- package/dist/features/stores/components/StoreHeader.js +1 -1
- package/dist/features/support/schemas/firestore.d.ts +73 -2
- package/dist/features/support/schemas/firestore.js +59 -0
- package/dist/index.d.ts +16 -24
- package/dist/index.js +38 -47
- package/dist/next/ErrorBoundary.js +1 -1
- package/dist/next/routing/route-map.d.ts +24 -0
- package/dist/next/routing/route-map.js +40 -0
- package/dist/providers/payment-manual/index.d.ts +15 -1
- package/dist/providers/payment-manual/index.js +37 -0
- package/dist/providers/payment-phonepe/index.d.ts +14 -1
- package/dist/providers/payment-phonepe/index.js +36 -0
- package/dist/providers/payment-razorpay/index.d.ts +44 -1
- package/dist/providers/payment-razorpay/index.js +99 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +18 -0
- package/dist/providers/shipping-shiprocket/index.js +18 -0
- package/dist/schemas/registry.d.ts +44 -11
- package/dist/server-entry.d.ts +2 -0
- package/dist/server-entry.js +27 -0
- package/dist/server.d.ts +42 -0
- package/dist/server.js +93 -0
- package/dist/styles.css +418 -149
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +15 -12
- package/dist/tokens/themes/default-light.js +14 -11
- package/dist/tokens/tokens.css +106 -23
- package/dist/ui/DataTable.style.css +4 -4
- package/dist/ui/columns/cell-renderers.d.ts +20 -0
- package/dist/ui/columns/cell-renderers.js +26 -1
- package/dist/ui/columns/index.d.ts +1 -1
- package/dist/ui/columns/index.js +4 -1
- package/dist/ui/components/Badge.style.css +1 -1
- package/dist/ui/components/Breadcrumb.js +19 -1
- package/dist/ui/components/BulkActionBar.style.css +4 -4
- package/dist/ui/components/Button.style.css +1 -1
- package/dist/ui/components/Checkbox.style.css +1 -1
- package/dist/ui/components/FallbackShell.js +1 -1
- package/dist/ui/components/Form.d.ts +14 -1
- package/dist/ui/components/Form.js +2 -2
- package/dist/ui/components/FormField.js +18 -4
- package/dist/ui/components/FormField.style.css +43 -3
- package/dist/ui/components/GatedPrice.style.css +1 -1
- package/dist/ui/components/IconButton.style.css +1 -1
- package/dist/ui/components/Input.js +22 -1
- package/dist/ui/components/Input.style.css +38 -22
- package/dist/ui/components/ListingFilterDrawer.js +1 -1
- package/dist/ui/components/ListingToolbar.js +1 -1
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/Radio.style.css +1 -1
- package/dist/ui/components/Repeater.d.ts +53 -0
- package/dist/ui/components/Repeater.js +7 -0
- package/dist/ui/components/RichTextEditor.d.ts +20 -1
- package/dist/ui/components/RichTextEditor.js +23 -2
- package/dist/ui/components/Select.style.css +7 -21
- package/dist/ui/components/Semantic.js +27 -3
- package/dist/ui/components/TagInput.style.css +4 -9
- package/dist/ui/components/Textarea.style.css +4 -18
- package/dist/ui/components/Toggle.style.css +1 -1
- package/dist/ui/components/Typography.d.ts +3 -3
- package/dist/ui/components/Typography.js +15 -15
- package/dist/ui/components/ViewToggle.style.css +4 -4
- package/dist/ui/forms/AddAnotherToggle.d.ts +16 -0
- package/dist/ui/forms/AddAnotherToggle.js +56 -0
- package/dist/ui/forms/ColorPickerField.js +15 -2
- package/dist/ui/forms/FieldCheckbox.js +15 -2
- package/dist/ui/forms/FieldInput.d.ts +22 -1
- package/dist/ui/forms/FieldInput.js +19 -5
- package/dist/ui/forms/FieldLabel.d.ts +35 -0
- package/dist/ui/forms/FieldLabel.js +70 -0
- package/dist/ui/forms/FieldSelect.d.ts +4 -1
- package/dist/ui/forms/FieldSelect.js +27 -5
- package/dist/ui/forms/FieldTextarea.d.ts +4 -1
- package/dist/ui/forms/FieldTextarea.js +22 -5
- package/dist/ui/forms/FormShell.d.ts +17 -1
- package/dist/ui/forms/FormShell.js +36 -2
- package/dist/ui/forms/FormShell.style.css +41 -33
- package/dist/ui/forms/index.d.ts +5 -1
- package/dist/ui/forms/index.js +2 -0
- package/dist/ui/icons/icon-registry.d.ts +1 -0
- package/dist/ui/icons/icon-registry.js +17 -1
- package/dist/utils/id-generators.d.ts +80 -0
- package/dist/utils/id-generators.js +93 -0
- package/dist/utils/search-txt-builders.d.ts +12 -0
- package/dist/utils/search-txt-builders.js +17 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +106 -5
- package/dist/features/loyalty/api/admin/loyalty/grant/route.d.ts +0 -12
- package/dist/features/loyalty/api/admin/loyalty/grant/route.js +0 -38
- package/dist/features/loyalty/api/balance/route.d.ts +0 -12
- package/dist/features/loyalty/api/balance/route.js +0 -41
- package/dist/features/loyalty/api/earn/route.d.ts +0 -12
- package/dist/features/loyalty/api/earn/route.js +0 -38
- package/dist/features/loyalty/api/history/route.d.ts +0 -12
- package/dist/features/loyalty/api/history/route.js +0 -58
- package/dist/features/loyalty/api/redeem/route.d.ts +0 -12
- package/dist/features/loyalty/api/redeem/route.js +0 -38
- package/dist/features/loyalty/columns/index.d.ts +0 -17
- package/dist/features/loyalty/columns/index.js +0 -40
- package/dist/features/loyalty/components/CoinsDisplay.d.ts +0 -12
- package/dist/features/loyalty/components/CoinsDisplay.js +0 -10
- package/dist/features/loyalty/components/index.d.ts +0 -1
- package/dist/features/loyalty/components/index.js +0 -1
- package/dist/features/loyalty/hooks/useLoyaltyBalance.d.ts +0 -4
- package/dist/features/loyalty/hooks/useLoyaltyBalance.js +0 -25
- package/dist/features/loyalty/index.d.ts +0 -6
- package/dist/features/loyalty/index.js +0 -6
- package/dist/features/loyalty/manifest.d.ts +0 -2
- package/dist/features/loyalty/manifest.js +0 -16
- package/dist/features/loyalty/messages/en.json +0 -19
- package/dist/features/loyalty/repository/loyalty.repository.d.ts +0 -8
- package/dist/features/loyalty/repository/loyalty.repository.js +0 -22
- package/dist/features/loyalty/schemas/index.d.ts +0 -85
- package/dist/features/loyalty/schemas/index.js +0 -40
- package/dist/features/loyalty/server.d.ts +0 -6
- package/dist/features/loyalty/server.js +0 -6
- package/dist/features/loyalty/types/index.d.ts +0 -31
- package/dist/features/loyalty/types/index.js +0 -1
- package/dist/features/loyalty/types/loyalty-config.d.ts +0 -7
- package/dist/features/loyalty/types/loyalty-config.js +0 -1
- package/dist/features/loyalty/types/loyalty-math.d.ts +0 -9
- package/dist/features/loyalty/types/loyalty-math.js +0 -27
- package/dist/features/products/components/ProductOwnerQuickActions.d.ts +0 -11
- package/dist/features/products/components/ProductOwnerQuickActions.js +0 -120
|
@@ -13,10 +13,9 @@ import { ApiError, ValidationError, NotFoundError, ERROR_MESSAGES } from "../../
|
|
|
13
13
|
import { ORDER_FIELDS } from "../../../../constants/field-names.js";
|
|
14
14
|
import { serverLogger } from "../../../../monitoring/index.js";
|
|
15
15
|
import { roundShippingUp, } from "../../../shared/shipping/quote.js";
|
|
16
|
-
import { pointsRepository } from "../../../../features/points/repository/points.repository.js";
|
|
17
|
-
import { maxRedeemablePoints } from "../../../../features/points/schemas/firestore.js";
|
|
18
16
|
import { unitOfWork, adminNotificationsRepository, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, productRepository, } from "../../../../repositories/index.js";
|
|
19
17
|
import { isIntraStateSupply } from "../../../shared/fees/calculator.js";
|
|
18
|
+
import { resolveStateKey, INDIA_STATES } from "../../../shared/fees/india-states.js";
|
|
20
19
|
import { computePreOrderDepositAmount, lineTotalFor, sumGroupGst, unitPriceFor, } from "../../../shared/checkout/order-math.js";
|
|
21
20
|
import { COD_MAX_ORDER_VALUE_FALLBACK } from "../../../../features/admin/schemas/firestore.js";
|
|
22
21
|
import { CHECKOUT_ERROR_CODES } from "../../../../errors/error-codes.js";
|
|
@@ -24,10 +23,17 @@ import { failedCheckoutRepository } from "../../../../features/checkout/reposito
|
|
|
24
23
|
import { sendOrderConfirmationEmail } from "../../../../features/contact/server.js";
|
|
25
24
|
import { splitCartIntoOrderGroups } from "../../../../features/orders/index.js";
|
|
26
25
|
import { DEFAULT_STORE_SHIPPING_POLICY, quoteShipping, toStoreShippingPolicy, } from "../../../shared/features/shipping/mode.js";
|
|
26
|
+
/*
|
|
27
|
+
* D44's first consumer of `shippingUnavailable`. A store whose shipping cannot
|
|
28
|
+
* be priced is a configuration fault with a money consequence — we ship free —
|
|
29
|
+
* so it belongs in the operator queue, keyed on the store so a hundred
|
|
30
|
+
* checkouts produce one case.
|
|
31
|
+
*/
|
|
32
|
+
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
27
33
|
import { computeProtectionFees } from "../../../shared/features/shipping/protections.js";
|
|
28
34
|
import { resolveCouponCategorySlugs } from "../../../../features/promotions/actions/coupon-actions.js";
|
|
29
35
|
import { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines.js";
|
|
30
|
-
import { activeLane, defaultCheckoutItems, laneOf, } from "../../../shared/checkout/lanes.js";
|
|
36
|
+
import { activeLane, defaultCheckoutItems, resolveCheckoutItems, laneOf, } from "../../../shared/checkout/lanes.js";
|
|
31
37
|
import { computeCodHandlingFee, computeGiftWrapFee, computeShipmentProtectionFee, computeSaleProtectionFee, computeCheckoutFees, allocateCheckoutFees, } from "../../../shared/fees/calculator.js";
|
|
32
38
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase/index.js";
|
|
33
39
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore.js";
|
|
@@ -51,6 +57,10 @@ import { checkEmiEligibility, computeBuyerEmiQuote, computeEmiSchedule } from ".
|
|
|
51
57
|
import { formatShippingAddress } from "./data.js";
|
|
52
58
|
import { enforceMaxPerUserForCart } from "./prize-bundle-gates.js";
|
|
53
59
|
import { getListingRule, runSyncPreflight, assertPaymentMethodAllowed, } from "../../../shared/checkout/rules/index.js";
|
|
60
|
+
// The `GroupCouponDiscountResult` type is deliberately NOT imported: every
|
|
61
|
+
// call site here destructures the result inline, so naming the type would be
|
|
62
|
+
// an unused import.
|
|
63
|
+
import { computeCartCouponDiscount, computeGroupCouponDiscount, } from "../../../shared/checkout/coupon-discount.js";
|
|
54
64
|
import { revalidateGroupLines } from "./group-rule-revalidation.js";
|
|
55
65
|
import { cartIsDigitalOnly } from "../../../shared/listing-types/cart-shipping.js";
|
|
56
66
|
import { processRefundAction } from "../refunds/actions.js";
|
|
@@ -60,6 +70,22 @@ import { resolveSupplierIdentity } from "./supplier-identity.js";
|
|
|
60
70
|
import { PAYMENT_WINDOW_MS } from "../../../../features/orders/constants/payment-window.js";
|
|
61
71
|
import { isCheckoutValueOtpVerified } from "../../../../features/checkout/actions/checkout-value-otp-actions.js";
|
|
62
72
|
const AUDIT_LOG_FAIL_MSG = "checkout: audit log failed (non-critical)";
|
|
73
|
+
/**
|
|
74
|
+
* How long a `gatewayOrderClaims` holder owns the right to place an order
|
|
75
|
+
* before another caller may take the claim over.
|
|
76
|
+
*
|
|
77
|
+
* 🛑 Sized against the SLOWEST legitimate placement, not the typical one. The
|
|
78
|
+
* route runs with `maxDuration = 60`, and a placement fans out to the gateway
|
|
79
|
+
* re-fetch, the cart read, product validation, stock decrements and N order
|
|
80
|
+
* writes — so a lease shorter than the work would let a second placer start
|
|
81
|
+
* while the first is still mid-write, which is the duplicate order the claim
|
|
82
|
+
* exists to prevent. 90s is that ceiling plus headroom.
|
|
83
|
+
*
|
|
84
|
+
* It is also the buyer's worst-case wait before a retry can succeed, which is
|
|
85
|
+
* the other reason not to make it generous: before this existed the wait was
|
|
86
|
+
* unbounded, because a failed placement left the claim forever.
|
|
87
|
+
*/
|
|
88
|
+
const GATEWAY_CLAIM_LEASE_MS = 90000;
|
|
63
89
|
/**
|
|
64
90
|
* SB-UNI-N — Atomically claim the next available digital code for a confirmed
|
|
65
91
|
* order. Pre-fetches a candidate code outside the transaction then locks and
|
|
@@ -196,7 +222,37 @@ function accumulateCouponUsage(accumulator, couponCode, couponId, discountAmount
|
|
|
196
222
|
* exact same per-seller shipping fee that order creation charges/records —
|
|
197
223
|
* a single source of truth instead of a third re-implementation.
|
|
198
224
|
*/
|
|
199
|
-
export async function resolveShippingCost(storeId
|
|
225
|
+
export async function resolveShippingCost(storeId,
|
|
226
|
+
/**
|
|
227
|
+
* The cart facts needed to QUOTE rather than merely resolve the policy.
|
|
228
|
+
*
|
|
229
|
+
* 🛑 D44 — OMITTING THIS IS TWO MONEY BUGS IN OPPOSITE DIRECTIONS, and until
|
|
230
|
+
* now every one of the five call sites omitted it.
|
|
231
|
+
*
|
|
232
|
+
* `quoteShipping` was called exactly once, here, with `goodsValue: 0` and
|
|
233
|
+
* `carrierRate: null`. Measured against its own logic
|
|
234
|
+
* (`shared/features/shipping/mode.ts`):
|
|
235
|
+
*
|
|
236
|
+
* 1. **Free-above-threshold can never fire.** The test is
|
|
237
|
+
* `goodsValue >= policy.freeAbove`, and `0 >= 999` is false for any
|
|
238
|
+
* positive threshold — so a store advertising "free shipping over ₹999"
|
|
239
|
+
* **charged its base rate on a ₹5,000 order.** The buyer is OVERCHARGED,
|
|
240
|
+
* and the seller's own offer silently does not exist.
|
|
241
|
+
* 2. **`estimate` mode ships FREE.** No carrier rate returns
|
|
242
|
+
* `kind: "unavailable"`, which the mapping below turns into
|
|
243
|
+
* `shippingFee: 0` — and `shippingUnavailable` had **zero consumers**, so
|
|
244
|
+
* nobody noticed. The platform/seller is UNDERCHARGED, which is what that
|
|
245
|
+
* field's own 🛑 warns about: "a zero there would ship every parcel free
|
|
246
|
+
* at our expense, silently, until somebody noticed."
|
|
247
|
+
*
|
|
248
|
+
* `flat` and `free` modes were unaffected, which is why this survived: the
|
|
249
|
+
* common cases are correct and only a store with a threshold or an estimate
|
|
250
|
+
* policy is mispriced.
|
|
251
|
+
*
|
|
252
|
+
* Still optional, because the one legitimate caller is the seller-settings
|
|
253
|
+
* surface that wants the POLICY and has no cart. Every money path passes it.
|
|
254
|
+
*/
|
|
255
|
+
opts) {
|
|
200
256
|
if (!storeId) {
|
|
201
257
|
return {
|
|
202
258
|
shippingFee: 0,
|
|
@@ -214,7 +270,11 @@ export async function resolveShippingCost(storeId) {
|
|
|
214
270
|
const store = await storeRepository.findById(storeId);
|
|
215
271
|
const storeOwnerId = store?.ownerId;
|
|
216
272
|
const storeEmiEnabled = store?.emiEnabled === true;
|
|
217
|
-
|
|
273
|
+
// `store` is passed in rather than re-fetched inside: G3 needs its `gstin`,
|
|
274
|
+
// and the document is already in hand one line above. A second
|
|
275
|
+
// `storeRepository` read here is what line ~504's comment warns against,
|
|
276
|
+
// and it would cost a Firestore read on every checkout (Rule #6).
|
|
277
|
+
const storeState = await resolveStoreState(storeId, store?.gstin);
|
|
218
278
|
/*
|
|
219
279
|
* ── C5b: the store's shipping MODE, not a flat number ──────────────────
|
|
220
280
|
*
|
|
@@ -240,17 +300,76 @@ export async function resolveShippingCost(storeId) {
|
|
|
240
300
|
? toStoreShippingPolicy(store.shippingPricing)
|
|
241
301
|
: await legacyFlatPolicy(storeOwnerId);
|
|
242
302
|
/*
|
|
243
|
-
* 🛑
|
|
244
|
-
*
|
|
245
|
-
* This
|
|
246
|
-
* that needs the charged amount calls `quoteShipping`
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* `shippingUnavailable
|
|
251
|
-
*
|
|
303
|
+
* 🛑 D44 FIXED HERE: the quote now uses the caller's real goods value.
|
|
304
|
+
*
|
|
305
|
+
* This comment used to read *"`goodsValue: 0` and no carrier rate here,
|
|
306
|
+
* deliberately… a caller that needs the charged amount calls `quoteShipping`
|
|
307
|
+
* with the group's goods value"*. The design was right and **no caller ever
|
|
308
|
+
* did it** — `quoteShipping` had exactly one call site, this one, and all
|
|
309
|
+
* five callers destructured `shippingFee` and ignored both `shippingPolicy`
|
|
310
|
+
* and `shippingUnavailable`. A seam nobody used is not a seam; it is a
|
|
311
|
+
* default, and this default mispriced two policy shapes (see the `opts`
|
|
312
|
+
* docstring above).
|
|
313
|
+
*
|
|
314
|
+
* Defaulting `goodsValue` to 0 keeps the policy-only callers behaving
|
|
315
|
+
* exactly as before, so this is additive for them and corrective for the
|
|
316
|
+
* money paths.
|
|
252
317
|
*/
|
|
253
|
-
const quote = quoteShipping({
|
|
318
|
+
const quote = quoteShipping({
|
|
319
|
+
policy,
|
|
320
|
+
goodsValue: opts?.goodsValue ?? 0,
|
|
321
|
+
carrierRate: opts?.carrierRate ?? null,
|
|
322
|
+
});
|
|
323
|
+
/*
|
|
324
|
+
* ── D44, the other half: `shippingUnavailable` gets its FIRST CONSUMER ────
|
|
325
|
+
*
|
|
326
|
+
* The field was declared, assigned twice, and read by nothing — its own 🛑
|
|
327
|
+
* said "the caller MUST act on this rather than trusting `shippingFee: 0`"
|
|
328
|
+
* and no caller did. So an `estimate`-mode store shipped every parcel free.
|
|
329
|
+
*
|
|
330
|
+
* 🛑 IT DOES NOT REFUSE THE CHECKOUT, and that is a deliberate choice rather
|
|
331
|
+
* than timidity. **There is no carrier-rate source in the system at all**:
|
|
332
|
+
* Shiprocket is authenticated but its create-order/AWB/rate calls are
|
|
333
|
+
* unverified with zero callers, so `carrierRate` can only ever be null today.
|
|
334
|
+
* Throwing here would therefore break checkout for every estimate-mode store
|
|
335
|
+
* outright — converting a silent revenue leak into a total outage for those
|
|
336
|
+
* sellers, which is the worse of the two.
|
|
337
|
+
*
|
|
338
|
+
* So the leak is made VISIBLE instead: one case per store (the dedupe key is
|
|
339
|
+
* the store, so a hundred checkouts produce one row), carrying the amount we
|
|
340
|
+
* failed to charge. Once somebody has measured how many stores are actually
|
|
341
|
+
* in `estimate` mode, refusing becomes the correct stricter behaviour — and
|
|
342
|
+
* that measurement is a one-line query, not a design question.
|
|
343
|
+
*
|
|
344
|
+
* Fire-and-forget: a case write must never fail the checkout it is
|
|
345
|
+
* reporting on.
|
|
346
|
+
*/
|
|
347
|
+
if (quote.kind === "unavailable" && opts?.goodsValue !== undefined) {
|
|
348
|
+
void paymentCasesRepository()
|
|
349
|
+
.openOrObserve({
|
|
350
|
+
kind: "shipping_unpriced",
|
|
351
|
+
subjectId: storeId,
|
|
352
|
+
severity: "warning",
|
|
353
|
+
title: `Shipping could not be priced — ${storeId} is shipping free`,
|
|
354
|
+
detail: [
|
|
355
|
+
`This store's shipping policy is \`estimate\` mode and no carrier rate`,
|
|
356
|
+
`was available, so the quote came back "unavailable" and the checkout`,
|
|
357
|
+
`charged ₹0 for shipping.`,
|
|
358
|
+
"",
|
|
359
|
+
`Store: ${storeId}`,
|
|
360
|
+
`Goods value on this order group: ${opts.goodsValue}`,
|
|
361
|
+
"",
|
|
362
|
+
`🛑 There is no carrier-rate integration wired, so this will recur for`,
|
|
363
|
+
`every order from this store. Fix by giving the store a \`flat\` or`,
|
|
364
|
+
`\`free\` policy in its shipping settings, or by wiring a rate source.`,
|
|
365
|
+
].join("\n"),
|
|
366
|
+
entityType: "store",
|
|
367
|
+
entityId: storeId,
|
|
368
|
+
})
|
|
369
|
+
.catch((err) => {
|
|
370
|
+
void normalizeError(err);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
254
373
|
return {
|
|
255
374
|
shippingFee: quote.kind === "charge" ? quote.amount : 0,
|
|
256
375
|
storeOwnerId: storeOwnerId ?? undefined,
|
|
@@ -302,60 +421,50 @@ async function legacyFlatPolicy(storeOwnerId) {
|
|
|
302
421
|
return { mode: { kind: "flat", amount } };
|
|
303
422
|
}
|
|
304
423
|
/** P-8 GST — the seller's registered/pickup state, used to determine intra- vs inter-state tax. */
|
|
305
|
-
async function resolveStoreState(storeId
|
|
424
|
+
async function resolveStoreState(storeId,
|
|
425
|
+
/**
|
|
426
|
+
* The store's GSTIN, supplied by the caller which already holds the store
|
|
427
|
+
* document. Not re-fetched here — see the call site.
|
|
428
|
+
*/
|
|
429
|
+
storeGstin) {
|
|
430
|
+
/*
|
|
431
|
+
* ── G3: the GSTIN's state code wins over the typed pickup address ────────
|
|
432
|
+
*
|
|
433
|
+
* A GSTIN's first two characters ARE the state of registration — it is
|
|
434
|
+
* issued per state, so for a registered seller it is the authoritative
|
|
435
|
+
* place of supply. The pickup address is free text a seller typed, and a
|
|
436
|
+
* store whose pickup state disagrees with its GSTIN prefix (a warehouse in
|
|
437
|
+
* another state, a typo, a seller who moved) splits the tax on the wrong
|
|
438
|
+
* one.
|
|
439
|
+
*
|
|
440
|
+
* `resolveStateKey` accepts a bare code, a full GSTIN, or a name, so the
|
|
441
|
+
* GSTIN goes to it whole rather than being sliced here — that keeps the
|
|
442
|
+
* "first two characters are the state" knowledge in exactly one place.
|
|
443
|
+
*
|
|
444
|
+
* 🛑 Falls back to the pickup address rather than failing: an UNREGISTERED
|
|
445
|
+
* seller legitimately has no GSTIN and is the common case on this platform.
|
|
446
|
+
* Returns the canonical display NAME, not the raw code, so every downstream
|
|
447
|
+
* reader still receives a state name.
|
|
448
|
+
*/
|
|
449
|
+
const fromGstin = resolveStateKey(storeGstin);
|
|
450
|
+
if (fromGstin)
|
|
451
|
+
return INDIA_STATES[fromGstin].name;
|
|
306
452
|
const addresses = await addressesRepository.listByOwner("store", storeId);
|
|
307
453
|
const pickup = addresses.find((a) => a.isDefault) ?? addresses[0];
|
|
308
454
|
return pickup?.state;
|
|
309
455
|
}
|
|
310
|
-
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
456
|
+
/*
|
|
457
|
+
* 🛑 `computeGroupCouponDiscount` and `GroupCouponDiscountResult` MOVED to
|
|
458
|
+
* `_internal/shared/checkout/coupon-discount.ts` on 2026-10-21. Imported at
|
|
459
|
+
* the top of this file; do not re-add a local copy.
|
|
460
|
+
*
|
|
461
|
+
* It was module-private here, which meant the three order paths in this file
|
|
462
|
+
* could reach it and `/api/payment/create-order` — the route that decides what
|
|
463
|
+
* the buyer's CARD is charged — could not, because a consumer route cannot
|
|
464
|
+
* deep-import `_internal/server`. That route consequently had no coupon term at
|
|
465
|
+
* all, so every coupon order paid by card over-captured by the discount. One
|
|
466
|
+
* implementation, four callers.
|
|
318
467
|
*/
|
|
319
|
-
function computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, storeId) {
|
|
320
|
-
let couponDiscount = 0;
|
|
321
|
-
const appliedDiscounts = [];
|
|
322
|
-
for (const coupon of appliedCoupons) {
|
|
323
|
-
let couponGroupDiscount = 0;
|
|
324
|
-
const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
|
|
325
|
-
if (isSellerScoped) {
|
|
326
|
-
if (coupon.storeId !== storeId)
|
|
327
|
-
continue;
|
|
328
|
-
if (coupon.applicableItemIds?.length) {
|
|
329
|
-
const eligibleTotal = groupLines
|
|
330
|
-
.filter((l) => coupon.applicableItemIds.includes(l.itemId))
|
|
331
|
-
.reduce((s, l) => s + l.lineTotal, 0);
|
|
332
|
-
couponGroupDiscount =
|
|
333
|
-
eligibleTotal > 0
|
|
334
|
-
? Math.min(roundRupees((eligibleTotal / groupTotal) * coupon.discountAmount), eligibleTotal)
|
|
335
|
-
: 0;
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
else if (cartSubtotal > 0) {
|
|
342
|
-
couponGroupDiscount = Math.min(roundRupees((groupTotal / cartSubtotal) * coupon.discountAmount), groupTotal);
|
|
343
|
-
}
|
|
344
|
-
if (couponGroupDiscount > 0) {
|
|
345
|
-
couponDiscount += couponGroupDiscount;
|
|
346
|
-
appliedDiscounts.push({
|
|
347
|
-
code: coupon.code,
|
|
348
|
-
couponId: coupon.couponId,
|
|
349
|
-
type: "coupon",
|
|
350
|
-
discountAmount: couponGroupDiscount,
|
|
351
|
-
scope: coupon.scope,
|
|
352
|
-
storeId: coupon.storeId,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
357
|
-
return { couponDiscount, appliedDiscounts };
|
|
358
|
-
}
|
|
359
468
|
function buildStockUpdatePayload(product, qtyDelta) {
|
|
360
469
|
const lt = (product.listingType ?? "standard");
|
|
361
470
|
const rule = getListingRule(lt);
|
|
@@ -500,6 +609,29 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
500
609
|
const { giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, saleProtectionAddon = false, } = storeAddons?.[firstItem.storeId] ?? {};
|
|
501
610
|
const { platformFee = 0, gstOnFee: platformFeeGst = 0 } = platformFeeByStore.get(firstItem.storeId) ?? {};
|
|
502
611
|
const groupTotal = group.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0);
|
|
612
|
+
/*
|
|
613
|
+
* ── G1: the discount is computed HERE, before the GST, on purpose ────────
|
|
614
|
+
*
|
|
615
|
+
* This used to sit ~150 lines below, after `sumGroupGst` had already backed
|
|
616
|
+
* the tax out of the UNDISCOUNTED gross — so the order recorded tax on money
|
|
617
|
+
* the buyer never paid (₹30.51 on a ₹1,000 item with a ₹200 coupon at 18%,
|
|
618
|
+
* verified).
|
|
619
|
+
*
|
|
620
|
+
* A discount shown on the invoice generally reduces the value of supply, so
|
|
621
|
+
* the pipeline order has to be goods → discount → tax, and that makes the
|
|
622
|
+
* computation's POSITION load-bearing rather than incidental. Moved up
|
|
623
|
+
* rather than duplicated: it is pure (no Firestore, no side effects — the
|
|
624
|
+
* usage-accumulator bookkeeping is layered on separately below), so hoisting
|
|
625
|
+
* it changes nothing but the order of two pure steps.
|
|
626
|
+
*
|
|
627
|
+
* 🛑 Do not move `sumGroupGst` above this, and do not recompute the discount
|
|
628
|
+
* below. Both re-open the defect.
|
|
629
|
+
*/
|
|
630
|
+
const groupLines = group.map(({ item, product }) => ({
|
|
631
|
+
itemId: item.itemId,
|
|
632
|
+
lineTotal: lineTotalFor(item, product),
|
|
633
|
+
}));
|
|
634
|
+
const { couponDiscount, appliedDiscounts } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
|
|
503
635
|
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
504
636
|
// A multi-member line expands into one row PER MEMBER sharing a groupSlug, so
|
|
505
637
|
// each row carries its own HSN/GST and can be cancelled independently; the
|
|
@@ -510,7 +642,15 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
510
642
|
return expandCartLineToOrderRows(item, product, ctx.productById).map((baseLine) => itemRule.decorateOrderItem(baseLine, product));
|
|
511
643
|
});
|
|
512
644
|
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
513
|
-
|
|
645
|
+
/*
|
|
646
|
+
* D44: the group's real goods value, so free-above-threshold can fire.
|
|
647
|
+
*
|
|
648
|
+
* `lineTotalFor`, never `item.price * item.quantity` — that is the
|
|
649
|
+
* hand-rolled rule Root Cause #75 is about, and on a locked-price or
|
|
650
|
+
* grouped line the two differ.
|
|
651
|
+
*/
|
|
652
|
+
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 });
|
|
514
654
|
/*
|
|
515
655
|
* 🛑 The buyer's quote WINS over the seller's configured rate, and it is
|
|
516
656
|
* re-rounded here rather than trusted as sent.
|
|
@@ -543,50 +683,22 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
543
683
|
// then split the group total into a single cgst/sgst/igst breakdown for the
|
|
544
684
|
// order document. Skipped entirely when GST is off or the buyer's state is
|
|
545
685
|
// unknown (digital-only carts have no shipping address).
|
|
546
|
-
let gstBreakdown;
|
|
547
|
-
if (gstSettings?.enabled && buyerState) {
|
|
548
|
-
/*
|
|
549
|
-
* 🛑 `isIntraStateSupply`, not `storeState === buyerState`. That raw
|
|
550
|
-
* comparison read "Karnataka" / "karnataka" / "KA" as three different
|
|
551
|
-
* states — the exact drift `constants/geo/subdivisions.ts` opens by
|
|
552
|
-
* documenting — and a store with no address resolved to `false`, i.e.
|
|
553
|
-
* silently inter-state. `null` means "cannot tell" and is logged rather
|
|
554
|
-
* than guessed; the AMOUNT is identical either way (IGST total ==
|
|
555
|
-
* CGST+SGST total), so what was wrong was the split on the invoice.
|
|
556
|
-
*/
|
|
557
|
-
const intra = isIntraStateSupply(storeState, buyerState);
|
|
558
|
-
if (intra === null) {
|
|
559
|
-
serverLogger.warn("[gst] place of supply undetermined — defaulting to inter-state (IGST)", { storeId: firstItem.storeId, hasStoreState: !!storeState });
|
|
560
|
-
}
|
|
561
|
-
// Per-member, not per-line: a bundle or grouped line spans several products
|
|
562
|
-
// with several different rates, and taxing the whole line at one product's
|
|
563
|
-
// rate is wrong in both directions. Shared with previewCheckoutPricing so
|
|
564
|
-
// the figure shown and the figure charged cannot drift.
|
|
565
|
-
const summary = sumGroupGst(group.map(({ item }) => item), ctx.productById, intra ?? false);
|
|
566
|
-
/*
|
|
567
|
-
* The split is taken AS COMPUTED, per slice, by `calculateGst` inside
|
|
568
|
-
* `sumGroupGst`. It used to be re-derived here as
|
|
569
|
-
* `Math.round(gstAmount / 2)` — whole rupees, so ₹161.82 became
|
|
570
|
-
* 81.00/80.82 rather than 80.91/80.91, and a ₹0.52 total rounded to 0 and
|
|
571
|
-
* was then dropped off the invoice entirely by the `|| undefined` below.
|
|
572
|
-
*
|
|
573
|
-
* The guard is on EITHER base: an order of purely exempt goods has a real
|
|
574
|
-
* taxable value to report and zero tax, and skipping it left the invoice
|
|
575
|
-
* unable to reconcile.
|
|
576
|
-
*/
|
|
577
|
-
if (summary.taxableAmount > 0 || summary.exemptAmount > 0) {
|
|
578
|
-
gstBreakdown = summary;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
686
|
const isCodLike = paymentMethod === "cod" || paymentMethod === "upi_manual" || paymentMethod === "cash";
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
687
|
+
/*
|
|
688
|
+
* 🛑 D43 — THE DEPOSIT/REMAINING SPLIT MOVED DOWN, BELOW `orderTotal`.
|
|
689
|
+
*
|
|
690
|
+
* It used to be computed HERE, on `groupTotal` — the goods value — so
|
|
691
|
+
* `depositAmount + codRemainingAmount` summed to the goods only and omitted
|
|
692
|
+
* shipping, the COD handling fee, add-ons, the platform fee and its GST.
|
|
693
|
+
* A COD buyer was told to "pay ₹X on delivery" where X was short by every
|
|
694
|
+
* fee they had just agreed to, and the courier would collect the wrong
|
|
695
|
+
* amount.
|
|
696
|
+
*
|
|
697
|
+
* It could not be fixed in place: `codHandlingFee` (below), `giftWrapFee`
|
|
698
|
+
* and both protection fees are all computed AFTER this point, so the total
|
|
699
|
+
* simply did not exist yet. The split now sits immediately after
|
|
700
|
+
* `orderTotal`, which is the only place every term is known.
|
|
701
|
+
*/
|
|
590
702
|
let emiSchedule;
|
|
591
703
|
if (paymentMethod === "emi" && emiTenureMonths) {
|
|
592
704
|
const eligibility = checkEmiEligibility(groupTotal, storeEmiEnabled, emiSettings);
|
|
@@ -626,9 +738,80 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
626
738
|
shippingFee: (s, sel) => computeShipmentProtectionFee(s, sel, commissions),
|
|
627
739
|
saleFee: (s, sel) => computeSaleProtectionFee(s, sel, commissions),
|
|
628
740
|
});
|
|
741
|
+
/*
|
|
742
|
+
* 🛑 THIS BLOCK WAS MOVED DOWN (2026-10-21) and its position is load-bearing.
|
|
743
|
+
*
|
|
744
|
+
* It used to sit ~90 lines above, before `codHandlingFee`, `giftWrapFee` and
|
|
745
|
+
* the two protection fees were computed — so it could not possibly tax them,
|
|
746
|
+
* and they entered the order total as bare addends carrying no tax (G5).
|
|
747
|
+
*
|
|
748
|
+
* It now runs after every ancillary charge is known and before `orderTotal`
|
|
749
|
+
* consumes them. Do not move it back up, and do not move a fee computation
|
|
750
|
+
* below it.
|
|
751
|
+
*/
|
|
752
|
+
let gstBreakdown;
|
|
753
|
+
if (gstSettings?.enabled && buyerState) {
|
|
754
|
+
/*
|
|
755
|
+
* 🛑 `isIntraStateSupply`, not `storeState === buyerState`. That raw
|
|
756
|
+
* comparison read "Karnataka" / "karnataka" / "KA" as three different
|
|
757
|
+
* states — the exact drift `constants/geo/subdivisions.ts` opens by
|
|
758
|
+
* documenting — and a store with no address resolved to `false`, i.e.
|
|
759
|
+
* silently inter-state. `null` means "cannot tell" and is logged rather
|
|
760
|
+
* than guessed; the AMOUNT is identical either way (IGST total ==
|
|
761
|
+
* CGST+SGST total), so what was wrong was the split on the invoice.
|
|
762
|
+
*/
|
|
763
|
+
const intra = isIntraStateSupply(storeState, buyerState);
|
|
764
|
+
if (intra === null) {
|
|
765
|
+
serverLogger.warn("[gst] place of supply undetermined — defaulting to inter-state (IGST)", { storeId: firstItem.storeId, hasStoreState: !!storeState });
|
|
766
|
+
}
|
|
767
|
+
// Per-member, not per-line: a bundle or grouped line spans several products
|
|
768
|
+
// with several different rates, and taxing the whole line at one product's
|
|
769
|
+
// rate is wrong in both directions. Shared with previewCheckoutPricing so
|
|
770
|
+
// the figure shown and the figure charged cannot drift.
|
|
771
|
+
const summary = sumGroupGst(group.map(({ item }) => item), ctx.productById, intra ?? false,
|
|
772
|
+
// G1 — the taxable value is the POST-discount gross. See the hoist above.
|
|
773
|
+
couponDiscount,
|
|
774
|
+
/*
|
|
775
|
+
* G5 — the ancillary charges, taxed at the principal rate. Shipping,
|
|
776
|
+
* COD handling, gift wrap and both protections; NOT the platform fee,
|
|
777
|
+
* which already carries its own `platformFeeGst` and would be taxed
|
|
778
|
+
* twice, and NOT the EMI surcharge, which is a financing charge with
|
|
779
|
+
* its own treatment and is deliberately outside the decided scope.
|
|
780
|
+
*/
|
|
781
|
+
roundRupees(shippingFee + codHandlingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee));
|
|
782
|
+
/*
|
|
783
|
+
* The split is taken AS COMPUTED, per slice, by `calculateGst` inside
|
|
784
|
+
* `sumGroupGst`. It used to be re-derived here as
|
|
785
|
+
* `Math.round(gstAmount / 2)` — whole rupees, so ₹161.82 became
|
|
786
|
+
* 81.00/80.82 rather than 80.91/80.91, and a ₹0.52 total rounded to 0 and
|
|
787
|
+
* was then dropped off the invoice entirely by the `|| undefined` below.
|
|
788
|
+
*
|
|
789
|
+
* The guard is on EITHER base: an order of purely exempt goods has a real
|
|
790
|
+
* taxable value to report and zero tax, and skipping it left the invoice
|
|
791
|
+
* unable to reconcile.
|
|
792
|
+
*/
|
|
793
|
+
if (summary.taxableAmount > 0 || summary.exemptAmount > 0) {
|
|
794
|
+
gstBreakdown = summary;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
629
797
|
const groupCouponCodes = new Set();
|
|
630
|
-
|
|
631
|
-
|
|
798
|
+
/*
|
|
799
|
+
* 🛑 `couponDiscount` / `appliedDiscounts` are HOISTED to just after
|
|
800
|
+
* `groupTotal` (~line 860), not computed here. The move is load-bearing:
|
|
801
|
+
* `sumGroupGst` sits between the two and now takes the discount, because the
|
|
802
|
+
* taxable value is the post-discount gross (G1). Recomputing here would be a
|
|
803
|
+
* second copy; moving the GST call below this would re-open the defect.
|
|
804
|
+
*
|
|
805
|
+
* What REMAINS here is the side effect — accumulating coupon usage — which
|
|
806
|
+
* must stay where it is, after the group has been confirmed placeable.
|
|
807
|
+
*
|
|
808
|
+
* Also fixed in the hoist: `groupLines` was built with
|
|
809
|
+
* `item.price * item.quantity` while `groupTotal` (155 lines above, same
|
|
810
|
+
* function) used `lineTotalFor`, and `computeGroupCouponDiscount` divides one
|
|
811
|
+
* by the other — so on a locked-price line the numerator carried the LIST
|
|
812
|
+
* price and the denominator the AGREED one, the ratio exceeded 1, and the
|
|
813
|
+
* coupon over-discounted that group. Root Cause #75, sixth copy.
|
|
814
|
+
*/
|
|
632
815
|
for (const discount of appliedDiscounts) {
|
|
633
816
|
if (discount.couponId) {
|
|
634
817
|
accumulateCouponUsage(couponUsageAccumulator, discount.code, discount.couponId, discount.discountAmount);
|
|
@@ -650,7 +833,49 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
650
833
|
* OUR service fee to the seller, charged to the buyer on top of the fee —
|
|
651
834
|
* not tax on the goods.
|
|
652
835
|
*/
|
|
653
|
-
|
|
836
|
+
/*
|
|
837
|
+
* 🛑 D42 — ROUNDED ONCE, HERE, AT THE END.
|
|
838
|
+
*
|
|
839
|
+
* This was a raw float sum of nine 2dp terms written verbatim to
|
|
840
|
+
* Firestore. IEEE-754 does not represent 0.1 exactly, so a nine-term sum
|
|
841
|
+
* lands on values like 1011.8000000000001 — and that is what got stored as
|
|
842
|
+
* the order's total, rendered on the invoice, and compared against the
|
|
843
|
+
* captured amount. The capture route was saved only because
|
|
844
|
+
* `rupeesToPaise` rounds on the way to the gateway; the Firestore write had
|
|
845
|
+
* no such protection.
|
|
846
|
+
*
|
|
847
|
+
* Rounded ONCE on the sum, never per term: rounding each addend first
|
|
848
|
+
* compounds nine half-paisa errors instead of one, which is how a total
|
|
849
|
+
* drifts from the sum of the lines shown beside it.
|
|
850
|
+
*/
|
|
851
|
+
roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0));
|
|
852
|
+
/*
|
|
853
|
+
* ── D43: the COD deposit/remaining split, on the ORDER TOTAL ──────────────
|
|
854
|
+
*
|
|
855
|
+
* Moved here from ~150 lines above, where it was computed on `groupTotal`
|
|
856
|
+
* and therefore summed to the goods value alone — see the 🛑 at its old
|
|
857
|
+
* site. Every term `orderTotal` needs is finally known at this point, which
|
|
858
|
+
* is why this is the only correct home for it.
|
|
859
|
+
*
|
|
860
|
+
* 🛑 The pre-order branch is deliberately left on `group`:
|
|
861
|
+
* `computePreOrderDepositAmount` reads each line's own
|
|
862
|
+
* `preOrderDepositPercent`, because a pre-order deposit is a per-PRODUCT
|
|
863
|
+
* commercial term the seller set, not a percentage of the order. Rebasing it
|
|
864
|
+
* on `orderTotal` would silently change what every existing pre-order
|
|
865
|
+
* charges — a different defect, not this one.
|
|
866
|
+
*
|
|
867
|
+
* `roundRupees` on both halves, and the remainder is taken as
|
|
868
|
+
* `orderTotal − deposit` rather than independently rounded, so the two
|
|
869
|
+
* provably sum to the total instead of drifting by a paisa (D42's shape).
|
|
870
|
+
*/
|
|
871
|
+
const depositAmount = !isCodLike
|
|
872
|
+
? undefined
|
|
873
|
+
: orderType === "preorder"
|
|
874
|
+
? computePreOrderDepositAmount(group, commissions.codDepositPercent)
|
|
875
|
+
: roundRupees(orderTotal * (commissions.codDepositPercent / 100));
|
|
876
|
+
const codRemainingAmount = isCodLike
|
|
877
|
+
? roundRupees(orderTotal - (depositAmount ?? 0))
|
|
878
|
+
: undefined;
|
|
654
879
|
const imageUrls = [
|
|
655
880
|
...new Set(group
|
|
656
881
|
.map(({ product }) => product.mainImage)
|
|
@@ -729,6 +954,16 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
729
954
|
// This group's pro-rata share of the checkout's single capped commission —
|
|
730
955
|
// the shares across a multi-store checkout sum to exactly what was charged.
|
|
731
956
|
platformFee: !adminBypass && platformFee > 0 ? platformFee : undefined,
|
|
957
|
+
/*
|
|
958
|
+
* Persisted alongside the fee, 2026-10-21. It was computed here, charged
|
|
959
|
+
* to the buyer as its own term in `orderTotal`, and then folded into
|
|
960
|
+
* `gstAmount` — so no field held the fee's own tax and a refund had to
|
|
961
|
+
* re-derive it from live percentages, which is how D32's ~10x
|
|
962
|
+
* over-deduction happened. The note above this function's `orderTotal`
|
|
963
|
+
* already explains why the two are kept separate; this persists the second
|
|
964
|
+
* one so a later reader does not have to reconstruct it.
|
|
965
|
+
*/
|
|
966
|
+
platformFeeGst: !adminBypass && platformFeeGst > 0 ? platformFeeGst : undefined,
|
|
732
967
|
giftWrapAddon: !adminBypass && giftWrapFee > 0 ? true : undefined,
|
|
733
968
|
giftWrapFee: !adminBypass && giftWrapFee > 0 ? giftWrapFee : undefined,
|
|
734
969
|
giftWrapMessage: !adminBypass && giftWrapFee > 0 ? giftWrapMessage?.slice(0, 500) : undefined,
|
|
@@ -782,6 +1017,14 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
782
1017
|
emiTenureMonths: !adminBypass && emiSchedule ? emiTenureMonths : undefined,
|
|
783
1018
|
emiTokenAmount: !adminBypass ? emiSchedule?.tokenAmount : undefined,
|
|
784
1019
|
emiSurchargeAmount: !adminBypass ? emiSchedule?.surchargeAmount : undefined,
|
|
1020
|
+
/*
|
|
1021
|
+
* D45: the seller's share of the surcharge, recorded for the first time.
|
|
1022
|
+
* `computeEmiSchedule` has always returned it and nothing ever stored it,
|
|
1023
|
+
* so a split the admin had configured (default 50%) was computed and
|
|
1024
|
+
* discarded on every EMI order. Persisting it moves no money — see the
|
|
1025
|
+
* field's own 🛑 — it just stops the decision being thrown away.
|
|
1026
|
+
*/
|
|
1027
|
+
emiSurchargeSellerShare: !adminBypass ? emiSchedule?.surchargeSellerShare : undefined,
|
|
785
1028
|
emiInstallments: !adminBypass && emiSchedule
|
|
786
1029
|
? emiSchedule.installments.map((i) => ({ index: i.index, dueDate: i.dueDate, amount: i.amount, status: "pending" }))
|
|
787
1030
|
: undefined,
|
|
@@ -1178,45 +1421,46 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1178
1421
|
}
|
|
1179
1422
|
await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
|
|
1180
1423
|
/*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
* The
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1424
|
+
* ── 🛑 THE POINTS REDEMPTION BLOCK WAS DELETED HERE, 2026-10-21 (D46) ─────
|
|
1425
|
+
*
|
|
1426
|
+
* It debited the buyer's points and reduced NOTHING, and its own comment
|
|
1427
|
+
* asserted the opposite. Preserved verbatim because the reasoning error is
|
|
1428
|
+
* the instructive part:
|
|
1429
|
+
*
|
|
1430
|
+
* "This ordering is the whole safety argument. … This way the worst case
|
|
1431
|
+
* is an order placed at full price with the balance intact."
|
|
1432
|
+
*
|
|
1433
|
+
* The balance was not intact. It was debited. That sentence reasons only
|
|
1434
|
+
* about the branch where `redeem` THROWS — in which case, yes, the buyer
|
|
1435
|
+
* keeps their points. The SUCCESS branch is the broken one: by the time this
|
|
1436
|
+
* ran, `createOrderForGroup` had already created every order and the gateway
|
|
1437
|
+
* had already captured `total`, so a successful redeem took points from a
|
|
1438
|
+
* buyer who had just paid full price. No order field, no cart field and no
|
|
1439
|
+
* gateway amount was ever reduced by `spend`.
|
|
1440
|
+
*
|
|
1441
|
+
* 🛑 It was unreachable, and that is the only reason it cost nothing: no
|
|
1442
|
+
* route schema, no caller and no UI ever passed `pointsToRedeem` — verified
|
|
1443
|
+
* across `src/` and `appkit/src/`. So the hazard was not the behaviour, it
|
|
1444
|
+
* was that ADDING ONE FIELD to one route schema would have made it live,
|
|
1445
|
+
* while `UserPointsView` already promises buyers "up to half of an order's
|
|
1446
|
+
* total can be paid with points". Somebody would have wired it.
|
|
1447
|
+
*
|
|
1448
|
+
* Deleting it leaves a promise with no mechanism, which is a disappointment.
|
|
1449
|
+
* Keeping it left a mechanism that silently takes points, which is a theft.
|
|
1450
|
+
*
|
|
1451
|
+
* ## What the real tender has to do instead
|
|
1452
|
+
*
|
|
1453
|
+
* Points are a TENDER, so they belong in the pricing pipeline, not after it:
|
|
1454
|
+
* a stage in `computeCheckoutTotal` AFTER rounding — `amountDue = total −
|
|
1455
|
+
* pointsApplied` — with the gateway capturing `amountDue` and the
|
|
1456
|
+
* post-capture guard comparing against `amountDue`. Anything else makes
|
|
1457
|
+
* points a sixth way the captured amount can diverge from the recorded order
|
|
1458
|
+
* total, which is the D37/D38/D40 family this plan exists to close.
|
|
1459
|
+
*
|
|
1460
|
+
* Everything that work needs now exists: `purchase_earn`, per-batch expiry,
|
|
1461
|
+
* FIFO consumption, `redemption_reversal` and the seller-funded deduction.
|
|
1462
|
+
* What is missing is the pipeline stage and the two document fields.
|
|
1193
1463
|
*/
|
|
1194
|
-
if (input.pointsToRedeem && input.pointsToRedeem > 0 && orderIds.length > 0) {
|
|
1195
|
-
try {
|
|
1196
|
-
const balance = await pointsRepository.getBalance(uid);
|
|
1197
|
-
const spend = maxRedeemablePoints(total, Math.min(balance, input.pointsToRedeem));
|
|
1198
|
-
if (spend > 0) {
|
|
1199
|
-
await pointsRepository.redeem({
|
|
1200
|
-
userId: uid,
|
|
1201
|
-
points: spend,
|
|
1202
|
-
orderId: orderIds[0],
|
|
1203
|
-
description: `Redeemed on ${orderIds[0]}`,
|
|
1204
|
-
});
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
catch (err) {
|
|
1208
|
-
/*
|
|
1209
|
-
* A failed redemption must not fail a placed order. The buyer keeps the
|
|
1210
|
-
* points and paid full price — the strictly safer side of this trade.
|
|
1211
|
-
*/
|
|
1212
|
-
const normalized = normalizeError(err);
|
|
1213
|
-
serverLogger.error("points redemption failed after order creation", {
|
|
1214
|
-
uid,
|
|
1215
|
-
orderId: orderIds[0],
|
|
1216
|
-
message: normalized.message,
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
1464
|
dispatchOrderConfirmationEmails(emailsToSend);
|
|
1221
1465
|
serverLogger.info(`createCheckoutOrderAction: ${orderIds.length} order(s) placed for uid=${uid}` +
|
|
1222
1466
|
(unavailable.length > 0 ? ` (${unavailable.length} item(s) unavailable)` : ""));
|
|
@@ -1315,7 +1559,14 @@ export async function previewCheckoutPricing(input) {
|
|
|
1315
1559
|
for (const { items: group } of orderGroups) {
|
|
1316
1560
|
const firstItem = group[0].item;
|
|
1317
1561
|
const groupTotal = group.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0);
|
|
1318
|
-
|
|
1562
|
+
/*
|
|
1563
|
+
* D44: `groupTotal` is already computed one line above with `lineTotalFor`
|
|
1564
|
+
* and was simply not being passed — so the PREVIEW told the buyer they owed
|
|
1565
|
+
* shipping on an order that qualified for the store's free-above threshold,
|
|
1566
|
+
* and then the order path (equally unpassed) charged it. Preview and charge
|
|
1567
|
+
* agreed, and both were wrong in the same direction.
|
|
1568
|
+
*/
|
|
1569
|
+
const { shippingFee: groupShippingFee, storeState, storeProtections, storeEmiEnabled } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
1319
1570
|
// Add-ons are per store, read off the cart document — the same entry
|
|
1320
1571
|
// createOrderForGroup will read when the order is actually placed.
|
|
1321
1572
|
const addons = cart.storeAddons?.[firstItem.storeId] ?? {};
|
|
@@ -1382,6 +1633,17 @@ export async function previewCheckoutPricing(input) {
|
|
|
1382
1633
|
shippingFee: (s, sel) => computeShipmentProtectionFee(s, sel, commissions),
|
|
1383
1634
|
saleFee: (s, sel) => computeSaleProtectionFee(s, sel, commissions),
|
|
1384
1635
|
});
|
|
1636
|
+
/*
|
|
1637
|
+
* G1 — the discount is computed BEFORE the GST block below, the same hoist
|
|
1638
|
+
* `createOrderForGroup` now has. It sat AFTER it until 2026-10-21, so the
|
|
1639
|
+
* tax figure SHOWN to the buyer was backed out of the undiscounted gross.
|
|
1640
|
+
*
|
|
1641
|
+
* The preview and the charge must agree on the taxable BASE, not only on
|
|
1642
|
+
* the total (Root Cause #59) — so the ordering has to match in both, which
|
|
1643
|
+
* makes the position of this block load-bearing rather than incidental.
|
|
1644
|
+
*/
|
|
1645
|
+
const groupLines = group.map(({ item, product }) => ({ itemId: item.itemId, lineTotal: lineTotalFor(item, product) }));
|
|
1646
|
+
const { couponDiscount: groupCouponDiscount } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
|
|
1385
1647
|
let groupGstAmount = 0;
|
|
1386
1648
|
if (siteSettings?.gst?.enabled && resolvedAddress?.state) {
|
|
1387
1649
|
/*
|
|
@@ -1395,10 +1657,23 @@ export async function previewCheckoutPricing(input) {
|
|
|
1395
1657
|
// Same helper createOrderForGroup uses — the two disagreeing is the
|
|
1396
1658
|
// recurring defect here (Root Cause #59), and it is the buyer seeing one
|
|
1397
1659
|
// tax figure and being charged another.
|
|
1398
|
-
groupGstAmount = sumGroupGst(group.map(({ item }) => item), resolvedProductById, intra ?? false
|
|
1660
|
+
groupGstAmount = sumGroupGst(group.map(({ item }) => item), resolvedProductById, intra ?? false, groupCouponDiscount,
|
|
1661
|
+
/*
|
|
1662
|
+
* G5 — the same five ancillary charges the order path taxes, in the
|
|
1663
|
+
* same order, so the figure SHOWN and the figure CHARGED cannot
|
|
1664
|
+
* diverge on the tax base (Root Cause #59). All five are already in
|
|
1665
|
+
* scope here, so unlike `createOrderForGroup` no block had to move.
|
|
1666
|
+
*
|
|
1667
|
+
* `groupEmiSurcharge` is deliberately absent — a financing charge with
|
|
1668
|
+
* its own treatment, outside the decided scope — and the platform fee
|
|
1669
|
+
* is absent because it already carries `platformFeeGst`.
|
|
1670
|
+
*/
|
|
1671
|
+
roundRupees(groupShippingFee +
|
|
1672
|
+
groupCodHandlingFee +
|
|
1673
|
+
groupGiftWrapFee +
|
|
1674
|
+
groupShipmentProtectionFee +
|
|
1675
|
+
groupSaleProtectionFee)).gstAmount;
|
|
1399
1676
|
}
|
|
1400
|
-
const groupLines = group.map(({ item, product }) => ({ itemId: item.itemId, lineTotal: lineTotalFor(item, product) }));
|
|
1401
|
-
const { couponDiscount: groupCouponDiscount } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
|
|
1402
1677
|
stores.push({
|
|
1403
1678
|
storeId: firstItem.storeId,
|
|
1404
1679
|
storeName: firstItem.storeName || firstItem.storeId,
|
|
@@ -1569,6 +1844,18 @@ async function refundDroppedItemsForPhonePeCheckout(input) {
|
|
|
1569
1844
|
*/
|
|
1570
1845
|
reasonCode: "not_received",
|
|
1571
1846
|
reasonNote: `Automatic refund — ${unavailablePaid.length} item(s) unavailable at checkout: ${unavailablePaid.map((u) => u.productTitle).join(", ")}`,
|
|
1847
|
+
/*
|
|
1848
|
+
* A NATURAL key, not a uuid — and this is a money fix in its own right,
|
|
1849
|
+
* not just satisfying a new required field.
|
|
1850
|
+
*
|
|
1851
|
+
* This path runs inside the gateway-settlement flow, which is itself
|
|
1852
|
+
* retried: by the browser's `/verify`, by the webhook, and now (Phase 0)
|
|
1853
|
+
* by the gateway's own 5xx retry. Every one of those retries used to
|
|
1854
|
+
* mint a fresh `randomUUID()` and issue ANOTHER real refund for the same
|
|
1855
|
+
* dropped items. Keying on the gateway order id makes the whole
|
|
1856
|
+
* auto-refund idempotent across every retry of the checkout.
|
|
1857
|
+
*/
|
|
1858
|
+
idempotencyKey: `oos_${phonepeOrderId}`,
|
|
1572
1859
|
method: "phonepe",
|
|
1573
1860
|
phonepeOrderId,
|
|
1574
1861
|
confirmIrrevocable: true,
|
|
@@ -1644,7 +1931,9 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
1644
1931
|
// Reuses the same store/seller lookup as the COD/UPI path above instead
|
|
1645
1932
|
// of re-implementing it inline — was two sequential findById calls per
|
|
1646
1933
|
// seller group here, duplicated from resolveShippingCost.
|
|
1647
|
-
|
|
1934
|
+
// D44: `groupTotal` is computed just above — pass it so a free-above
|
|
1935
|
+
// threshold can fire and an unpriceable store is recorded.
|
|
1936
|
+
const { shippingFee, storeOwnerId, storeState, storeTaxIdentity, storeProtections } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
1648
1937
|
/*
|
|
1649
1938
|
* Same supplier-of-record rule as `createOrderForGroup`, through the same
|
|
1650
1939
|
* function. This path (PhonePe verify / webhook) is the SECOND place an
|
|
@@ -1743,14 +2032,28 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
1743
2032
|
if (intra === null) {
|
|
1744
2033
|
serverLogger.warn("[gst] place of supply undetermined on the online path — defaulting to inter-state (IGST)", { storeId: firstItem.storeId, hasStoreState: !!storeState });
|
|
1745
2034
|
}
|
|
1746
|
-
|
|
2035
|
+
// G1 — post-discount taxable base. `couponDiscount` is already computed
|
|
2036
|
+
// ~100 lines above in this function, so unlike the other two call sites no
|
|
2037
|
+
// hoist was needed here; only the argument was missing.
|
|
2038
|
+
const summary = sumGroupGst(group.map(({ item }) => item), phonepeProductById, intra ?? false, couponDiscount,
|
|
2039
|
+
/*
|
|
2040
|
+
* G5 — the ancillary charges. 🛑 FOUR, not five: this is the ONLINE
|
|
2041
|
+
* payment path, so there is no `codHandlingFee` in scope and none is
|
|
2042
|
+
* charged — COD is a different payment method handled by
|
|
2043
|
+
* `createOrderForGroup`. The absence is correct, not an omission, and
|
|
2044
|
+
* worth saying so because the other two call sites pass five and a
|
|
2045
|
+
* reader comparing them would otherwise look for the missing term.
|
|
2046
|
+
*/
|
|
2047
|
+
roundRupees(shippingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee));
|
|
1747
2048
|
if (summary.taxableAmount > 0 || summary.exemptAmount > 0)
|
|
1748
2049
|
phonepeGst = summary;
|
|
1749
2050
|
}
|
|
1750
2051
|
/* Product GST is not a term — prices are tax-inclusive, so it is already
|
|
1751
2052
|
* inside `groupTotal`. Matches the COD/UPI path exactly; `platformFeeGst`
|
|
1752
2053
|
* stays because that is GST on our fee, charged on top. */
|
|
1753
|
-
|
|
2054
|
+
// D42: rounded once on the sum — see the COD/UPI path's 🛑 for why a raw
|
|
2055
|
+
// float sum of seven 2dp terms reached Firestore as 1011.8000000000001.
|
|
2056
|
+
const orderTotal = roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst);
|
|
1754
2057
|
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
1755
2058
|
// Same expansion as the COD/UPI path. This was a THIRD hand-rolled copy of
|
|
1756
2059
|
// the price rule (`isBundle ? item.price : product.price`) that — like the
|
|
@@ -1833,6 +2136,10 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
1833
2136
|
shippingAddress,
|
|
1834
2137
|
notes,
|
|
1835
2138
|
platformFee,
|
|
2139
|
+
// Persisted alongside the fee — see the COD/UPI path's note. Both paths
|
|
2140
|
+
// charged this to the buyer and recorded only the combined `gstAmount`,
|
|
2141
|
+
// leaving the refund path to re-derive it from live percentages (D32).
|
|
2142
|
+
platformFeeGst: platformFeeGst > 0 ? platformFeeGst : undefined,
|
|
1836
2143
|
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
1837
2144
|
giftWrapAddon: giftWrapFee > 0 ? true : undefined,
|
|
1838
2145
|
giftWrapFee: giftWrapFee > 0 ? giftWrapFee : undefined,
|
|
@@ -1942,15 +2249,39 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
1942
2249
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PAYMENT_FAILED);
|
|
1943
2250
|
}
|
|
1944
2251
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2252
|
+
/*
|
|
2253
|
+
* Idempotency: the buyer's /verify call and the async webhook can both race
|
|
2254
|
+
* to confirm+place for the same payment (see the module comment above).
|
|
2255
|
+
* `.create()` throws on conflict, so whichever caller creates this claim doc
|
|
2256
|
+
* first is the sole placer; the loser returns whatever the winner already
|
|
2257
|
+
* placed.
|
|
2258
|
+
*
|
|
2259
|
+
* 🛑 THE CLAIM CARRIES A LEASE, AND THAT IS NOT A REFINEMENT — IT IS THE
|
|
2260
|
+
* WHOLE POINT. Measured in production 2026-10-21: `order_TqJvzY4gVjR2cc` /
|
|
2261
|
+
* `pay_TqJx88j8OB7dSJ`, ₹2,511.80 captured by UPI, no order, and a manual
|
|
2262
|
+
* refund from the Razorpay dashboard.
|
|
2263
|
+
*
|
|
2264
|
+
* The old version wrote `{ claimedAt }` and nothing else. Everything below
|
|
2265
|
+
* this block — ~400 lines, a dozen throw sites, `ADDRESS_REQUIRED` and the
|
|
2266
|
+
* amount guard among them — runs AFTER the claim is taken, and **nothing
|
|
2267
|
+
* released it on failure**. So one failed placement made the claim a
|
|
2268
|
+
* tombstone: every later attempt hit ALREADY_EXISTS, found zero orders, and
|
|
2269
|
+
* returned "still being confirmed — please refresh". Refreshing could never
|
|
2270
|
+
* work. The error told the buyer to do the one thing guaranteed to fail.
|
|
2271
|
+
*
|
|
2272
|
+
* A lease fixes it without a 400-line try/catch, and fixes strictly more: a
|
|
2273
|
+
* `catch` cannot run if the process is killed mid-placement, and an expired
|
|
2274
|
+
* lease is reclaimable either way. The loser now distinguishes three states
|
|
2275
|
+
* rather than two — placed, genuinely in flight, or abandoned.
|
|
2276
|
+
*/
|
|
1951
2277
|
const claimRef = getAdminDb().collection("gatewayOrderClaims").doc(merchantOrderId);
|
|
2278
|
+
const claimNow = new Date();
|
|
2279
|
+
const claimLease = {
|
|
2280
|
+
claimedAt: claimNow,
|
|
2281
|
+
leaseExpiresAt: new Date(claimNow.getTime() + GATEWAY_CLAIM_LEASE_MS),
|
|
2282
|
+
};
|
|
1952
2283
|
try {
|
|
1953
|
-
await claimRef.create(
|
|
2284
|
+
await claimRef.create(claimLease);
|
|
1954
2285
|
}
|
|
1955
2286
|
catch (claimErr) {
|
|
1956
2287
|
void normalizeError(claimErr);
|
|
@@ -1962,7 +2293,45 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
1962
2293
|
itemCount: existingOrders.length,
|
|
1963
2294
|
};
|
|
1964
2295
|
}
|
|
1965
|
-
|
|
2296
|
+
/*
|
|
2297
|
+
* No order, and the holder's lease has run out: the previous attempt died
|
|
2298
|
+
* or threw. Take the claim over rather than refusing forever.
|
|
2299
|
+
*
|
|
2300
|
+
* The takeover is a TRANSACTION, not a bare update, because two retries
|
|
2301
|
+
* can arrive together — and two placers for one payment is the duplicate
|
|
2302
|
+
* order this claim exists to prevent. Re-reading inside the transaction
|
|
2303
|
+
* means exactly one of them wins the takeover; the other sees a fresh
|
|
2304
|
+
* lease and backs off.
|
|
2305
|
+
*/
|
|
2306
|
+
const tookOver = await getAdminDb()
|
|
2307
|
+
.runTransaction(async (tx) => {
|
|
2308
|
+
const snap = await tx.get(claimRef);
|
|
2309
|
+
const held = snap.data();
|
|
2310
|
+
const expiresAt = held?.leaseExpiresAt?.toDate?.();
|
|
2311
|
+
// A claim written before this field existed has no lease — treat it as
|
|
2312
|
+
// expired rather than immortal, which is the bug being fixed. Every
|
|
2313
|
+
// tombstone already in the collection is reclaimed by this line.
|
|
2314
|
+
if (expiresAt && expiresAt.getTime() > Date.now())
|
|
2315
|
+
return false;
|
|
2316
|
+
tx.update(claimRef, {
|
|
2317
|
+
claimedAt: new Date(),
|
|
2318
|
+
leaseExpiresAt: new Date(Date.now() + GATEWAY_CLAIM_LEASE_MS),
|
|
2319
|
+
// Incremented in JS rather than with `FieldValue.increment`: the
|
|
2320
|
+
// read is already inside this transaction, so it serialises
|
|
2321
|
+
// correctly — and `firebase-admin/firestore` is loaded lazily here
|
|
2322
|
+
// to keep it out of client graphs (Root Cause #24).
|
|
2323
|
+
takeoverCount: (held?.takeoverCount ?? 0) + 1,
|
|
2324
|
+
});
|
|
2325
|
+
return true;
|
|
2326
|
+
})
|
|
2327
|
+
.catch((txErr) => {
|
|
2328
|
+
void normalizeError(txErr);
|
|
2329
|
+
return false;
|
|
2330
|
+
});
|
|
2331
|
+
if (!tookOver) {
|
|
2332
|
+
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.ORDER_PLACEMENT_IN_PROGRESS);
|
|
2333
|
+
}
|
|
2334
|
+
serverLogger.warn("gateway claim taken over from an expired lease", { merchantOrderId });
|
|
1966
2335
|
}
|
|
1967
2336
|
/*
|
|
1968
2337
|
* THE authoritative check, for either gateway: re-fetch the order from the
|
|
@@ -2024,15 +2393,36 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2024
2393
|
if (!cart.items || cart.items.length === 0) {
|
|
2025
2394
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
2026
2395
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2396
|
+
/*
|
|
2397
|
+
* Same lane priority + locked-line revalidation the manual/COD path runs.
|
|
2398
|
+
*
|
|
2399
|
+
* 🛑 D40 — THIS LINE USED TO IGNORE THE BUYER'S SELECTION, and the capture
|
|
2400
|
+
* route did not. `defaultCheckoutItems(cart.items)` is the active lane and
|
|
2401
|
+
* nothing else, while `/api/payment/create-order` filtered by
|
|
2402
|
+
* `cart.selectedItemIds` and fell back to the WHOLE CART across every lane.
|
|
2403
|
+
* Two divergences fell out of that:
|
|
2404
|
+
*
|
|
2405
|
+
* with a selection → charged for the selected items, orders for the whole
|
|
2406
|
+
* lane: goods and stock leaving without payment.
|
|
2407
|
+
* no selection → charged for EVERY lane, orders for one: the buyer
|
|
2408
|
+
* OVERPAYS, and multi-lane carts are the normal product
|
|
2409
|
+
* of the lane model (see `defaultCheckoutItems`).
|
|
2410
|
+
*
|
|
2411
|
+
* `resolveCheckoutItems` is now the one definition both paths call: active
|
|
2412
|
+
* lane first, then the selection as a filter within it. Everything
|
|
2413
|
+
* downstream still reads `settleItems`, because stock decrements,
|
|
2414
|
+
* jurisdiction checks and the order rows must cover exactly what was charged.
|
|
2415
|
+
*/
|
|
2416
|
+
const settleItems = resolveCheckoutItems(cart.items, cart.selectedItemIds);
|
|
2417
|
+
if (settleItems.length === 0) {
|
|
2418
|
+
/*
|
|
2419
|
+
* An empty intersection is a refusal, not a fallback. It means the
|
|
2420
|
+
* selection names only off-lane items — so the buyer is looking at
|
|
2421
|
+
* something the server will not sell them, and widening to the whole lane
|
|
2422
|
+
* would charge them for lines they deliberately unticked.
|
|
2423
|
+
*/
|
|
2424
|
+
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
2425
|
+
}
|
|
2036
2426
|
assertCheckoutLane(cart.items, settleItems);
|
|
2037
2427
|
await assertLockedLinesStillValid(settleItems, uid);
|
|
2038
2428
|
const isDigitalCartPp = cartIsDigitalOnly(settleItems);
|
|
@@ -2125,13 +2515,25 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2125
2515
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.INSUFFICIENT_STOCK);
|
|
2126
2516
|
}
|
|
2127
2517
|
{
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2518
|
+
/*
|
|
2519
|
+
* 🛑 D38 — this guard used to HAND-ROLL the price rule it is guarding:
|
|
2520
|
+
*
|
|
2521
|
+
* const isBundle = Boolean(item.bundleCategorySlug && item.bundleProductIds?.length);
|
|
2522
|
+
* const unit = isBundle ? item.price : product!.price;
|
|
2523
|
+
*
|
|
2524
|
+
* It reproduced the BUNDLE branch of `unitPriceFor` and omitted the
|
|
2525
|
+
* `lockedPrice` and `groupMembers` ones — which is precisely what that
|
|
2526
|
+
* function's docstring forbids, and the fifth copy of the rule found
|
|
2527
|
+
* across three sessions (Root Cause #75).
|
|
2528
|
+
*
|
|
2529
|
+
* The failure was the worst available shape. On an accepted ₹800 offer for
|
|
2530
|
+
* a ₹1,000 listing: the capture route takes ₹800 (it uses `lineTotalFor`),
|
|
2531
|
+
* this guard expects ₹1,000, `paid < expected − 1` fires, and the function
|
|
2532
|
+
* THROWS — after the money has already been taken, on a branch with no
|
|
2533
|
+
* refund. The buyer is charged and gets no order. That is the D64 incident
|
|
2534
|
+
* class, reached by a different door.
|
|
2535
|
+
*/
|
|
2536
|
+
const cartSubtotalRs = productChecks.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0);
|
|
2135
2537
|
// Must use the SAME helper /api/payment/create-order used to compute what
|
|
2136
2538
|
// it charged. This block used to re-derive the fee inline from the raw
|
|
2137
2539
|
// percentages, so the moment the cap landed it would have expected more
|
|
@@ -2143,7 +2545,45 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2143
2545
|
// below will use to build each order's recorded totalPrice) — keeps this
|
|
2144
2546
|
// floor check from passing an amount that's short by the shipping fee.
|
|
2145
2547
|
const expectedShippingGroups = splitCartIntoOrderGroups(bucketedPaid.available);
|
|
2146
|
-
|
|
2548
|
+
/*
|
|
2549
|
+
* The coupon, which this guard also omitted — the mirror of D37, and the
|
|
2550
|
+
* two MUST move together. With the capture route now correctly deducting
|
|
2551
|
+
* the discount, a guard that does not would expect MORE than was collected
|
|
2552
|
+
* and would reject every legitimate coupon payment — turning an overcharge
|
|
2553
|
+
* into a post-capture throw, which is strictly worse.
|
|
2554
|
+
*
|
|
2555
|
+
* Grouped identically to the capture route (`splitCartIntoOrderGroups` over
|
|
2556
|
+
* the same available items), so the two compute the same number from the
|
|
2557
|
+
* same shape rather than agreeing by coincidence.
|
|
2558
|
+
*/
|
|
2559
|
+
const { couponDiscount: expectedCouponDiscount } = computeCartCouponDiscount(cart.appliedCoupons ?? [], expectedShippingGroups.map((g) => ({
|
|
2560
|
+
storeId: g.items[0].item.storeId,
|
|
2561
|
+
lines: g.items.map(({ item, product }) => ({
|
|
2562
|
+
itemId: item.itemId,
|
|
2563
|
+
lineTotal: lineTotalFor(item, product),
|
|
2564
|
+
})),
|
|
2565
|
+
})));
|
|
2566
|
+
const expectedDiscountedSubtotal = Math.max(0, cartSubtotalRs - expectedCouponDiscount);
|
|
2567
|
+
const expectedShippingFees = await Promise.all(
|
|
2568
|
+
/*
|
|
2569
|
+
* 🛑 D44 — THE GUARD MUST PASS `goodsValue` TOO, AND IN THIS SAME
|
|
2570
|
+
* COMMIT. This is the D37/D38 sequencing trap, one defect over.
|
|
2571
|
+
*
|
|
2572
|
+
* The capture route and both order paths now quote with the group's real
|
|
2573
|
+
* goods value, so a store's free-above threshold can fire and shipping
|
|
2574
|
+
* can legitimately be ₹0. A guard still quoting with `goodsValue: 0`
|
|
2575
|
+
* would compute the UN-thresholded fee, expect more than was collected,
|
|
2576
|
+
* trip `paid < expected − 1`, and **throw after the money was taken** —
|
|
2577
|
+
* converting a fixed overcharge into a charged-with-no-order, which is
|
|
2578
|
+
* the D64 incident class and strictly worse than the bug being fixed.
|
|
2579
|
+
*
|
|
2580
|
+
* Verified by construction rather than by hope: the goods value here is
|
|
2581
|
+
* computed with `lineTotalFor` over the same `expectedShippingGroups`
|
|
2582
|
+
* the capture route groups, so the two cannot disagree about the base.
|
|
2583
|
+
*/
|
|
2584
|
+
expectedShippingGroups.map((g) => resolveShippingCost(g.items[0].item.storeId, {
|
|
2585
|
+
goodsValue: g.items.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0),
|
|
2586
|
+
})));
|
|
2147
2587
|
const expectedShippingFee = expectedShippingFees.reduce((sum, r) => sum + r.shippingFee, 0);
|
|
2148
2588
|
// Add-ons are per store, so the expected total sums each group's own
|
|
2149
2589
|
// selection — a single cart-wide figure would over- or under-count as soon
|
|
@@ -2185,11 +2625,44 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2185
2625
|
* would make this guard demand ~₹580 more than the buyer owes on a
|
|
2186
2626
|
* ₹5,547 cart — rejecting every correctly-charged order.
|
|
2187
2627
|
*/
|
|
2188
|
-
const expectedPaymentAmountRs =
|
|
2628
|
+
const expectedPaymentAmountRs = expectedDiscountedSubtotal +
|
|
2629
|
+
expectedPlatformFee +
|
|
2630
|
+
expectedGstOnFee +
|
|
2631
|
+
expectedAddonFees +
|
|
2632
|
+
expectedShippingFee;
|
|
2189
2633
|
// Reuses the order already fetched above to confirm COMPLETED — PhonePe's
|
|
2190
2634
|
// 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
|
|
2191
2635
|
const paidAmountRs = paiseToRupees(gatewayOrder.amount);
|
|
2192
|
-
|
|
2636
|
+
/*
|
|
2637
|
+
* ── TWO-SIDED, and the second side is how D37 survived ─────────────────
|
|
2638
|
+
*
|
|
2639
|
+
* 🛑 This read `paidAmountRs < expectedPaymentAmountRs - 1` — a FLOOR
|
|
2640
|
+
* check only. An over-capture of any size passed silently, which is
|
|
2641
|
+
* exactly what let the missing coupon term go unnoticed: the gateway took
|
|
2642
|
+
* the undiscounted amount, this guard saw "paid ≥ expected" and waved it
|
|
2643
|
+
* through, and the order recorded the discounted figure. Nothing in our
|
|
2644
|
+
* data disagreed.
|
|
2645
|
+
*
|
|
2646
|
+
* A ceiling is now checked too. The tolerance stays asymmetric and that is
|
|
2647
|
+
* deliberate rather than sloppy:
|
|
2648
|
+
*
|
|
2649
|
+
* UNDER by > ₹1 → we were short-changed. Refuse.
|
|
2650
|
+
* OVER by > ₹1 → the BUYER was over-charged. Record loudly, and
|
|
2651
|
+
* PROCEED to place the order.
|
|
2652
|
+
*
|
|
2653
|
+
* Proceeding on an over-capture is the important choice. The alternative —
|
|
2654
|
+
* throwing — is what D38 did, and it leaves the buyer charged with no
|
|
2655
|
+
* order and no refund on that branch, which is the D64 incident class.
|
|
2656
|
+
* Placing the order means the buyer has what they paid for and the surplus
|
|
2657
|
+
* is a recorded, refundable discrepancy rather than a vanished payment.
|
|
2658
|
+
* Phase 4's reconciler reads `amount_mismatch` rows and opens a case; the
|
|
2659
|
+
* refund itself waits for Phase 1's idempotency key, because an automatic
|
|
2660
|
+
* refund on top of a non-idempotent refund path is a double-refund
|
|
2661
|
+
* generator.
|
|
2662
|
+
*/
|
|
2663
|
+
const amountDelta = paidAmountRs - expectedPaymentAmountRs;
|
|
2664
|
+
const AMOUNT_TOLERANCE_RS = 1;
|
|
2665
|
+
if (amountDelta < -AMOUNT_TOLERANCE_RS) {
|
|
2193
2666
|
serverLogger.warn(`Payment amount mismatch for user ${uid}: paid ₹${paidAmountRs}, expected ≥ ₹${expectedPaymentAmountRs}`);
|
|
2194
2667
|
failedCheckoutRepository
|
|
2195
2668
|
.logPayment(uid, "amount_mismatch", `Paid ₹${paidAmountRs}, expected ≥ ₹${expectedPaymentAmountRs}`, {
|
|
@@ -2201,6 +2674,18 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2201
2674
|
.catch((err) => { void normalizeError(err); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: err instanceof Error ? err.message : String(err) }); });
|
|
2202
2675
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PAYMENT_FAILED);
|
|
2203
2676
|
}
|
|
2677
|
+
if (amountDelta > AMOUNT_TOLERANCE_RS) {
|
|
2678
|
+
serverLogger.error(`OVER-CAPTURE for user ${uid}: paid ₹${paidAmountRs}, expected ₹${expectedPaymentAmountRs} (surplus ₹${amountDelta.toFixed(2)}) — order will still be placed`, { merchantOrderId, expectedCouponDiscount });
|
|
2679
|
+
failedCheckoutRepository
|
|
2680
|
+
.logPayment(uid, "amount_mismatch", `OVER-CAPTURE: paid ₹${paidAmountRs}, expected ₹${expectedPaymentAmountRs}, surplus ₹${amountDelta.toFixed(2)} — refund owed`, {
|
|
2681
|
+
gatewayOrderId: gatewayOrder.gatewayOrderId,
|
|
2682
|
+
gatewayPaymentId: merchantOrderId,
|
|
2683
|
+
amountRs: paidAmountRs,
|
|
2684
|
+
addressId,
|
|
2685
|
+
})
|
|
2686
|
+
.catch((err) => { void normalizeError(err); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: err instanceof Error ? err.message : String(err) }); });
|
|
2687
|
+
// Deliberately NO throw — see above.
|
|
2688
|
+
}
|
|
2204
2689
|
}
|
|
2205
2690
|
const { coupons: appliedCoupons, dropped: droppedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], bucketedPaid.available, uid);
|
|
2206
2691
|
// "skip_items" (or an all-available cart under any policy) — build order
|