@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
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment reconciliation — R1 through R4.
|
|
3
|
+
*
|
|
4
|
+
* ## What this is for
|
|
5
|
+
*
|
|
6
|
+
* Every other check in this system starts from OUR records and asks whether
|
|
7
|
+
* they are internally consistent. R2 starts from the **gateway's** records and
|
|
8
|
+
* asks whether we have an order at all — and that is the only direction that
|
|
9
|
+
* could have detected the 2026-10-21 incident, where our side had nothing to be
|
|
10
|
+
* inconsistent with. ₹2,511.80 was captured, no order existed, and the first
|
|
11
|
+
* signal was the buyer asking where their order was. **Had R2 existed it would
|
|
12
|
+
* have been a case on a dashboard within a day.**
|
|
13
|
+
*
|
|
14
|
+
* ## Cost, honestly
|
|
15
|
+
*
|
|
16
|
+
* One new Cloud Scheduler job (`paymentReconcile`, daily 05:30 UTC). Scheduler
|
|
17
|
+
* bills **per registered job** — 3 free, 28 already registered — so this is
|
|
18
|
+
* ~$0.10/month. It is NOT folded into `countersReconcile`, which already runs
|
|
19
|
+
* at its 540 s ceiling, nor into `autoPayoutEligibility`: `priceIndexRollup`'s
|
|
20
|
+
* own comment sets the precedent that a job should be able to fail without
|
|
21
|
+
* taking an unrelated one down with it.
|
|
22
|
+
*
|
|
23
|
+
* R1 is different and is folded into the existing `paymentWindowTimeout`
|
|
24
|
+
* (every 5 minutes) for free, because a stuck event needs a tight loop and the
|
|
25
|
+
* query is one indexed read.
|
|
26
|
+
*
|
|
27
|
+
* Reads per daily run, pre-launch: well under 50 against a 50,000/day free
|
|
28
|
+
* quota. **The count is logged on every run** — the same discipline
|
|
29
|
+
* `searchIndexBuild` uses — so a regression to always-full is visible before it
|
|
30
|
+
* is a quota wall. Root Cause #92 and the 2026-10-09 HTTP 402 are both what
|
|
31
|
+
* happens when nobody logs the number.
|
|
32
|
+
*/
|
|
33
|
+
import { getProviders } from "../../../../contracts/registry.js";
|
|
34
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
35
|
+
import { gatewayEventsRepository } from "../../../../features/gateway-events/repository/gateway-events.repository.js";
|
|
36
|
+
import { GATEWAY_EVENT_MAX_ATTEMPTS, } from "../../../../features/gateway-events/schemas/firestore.js";
|
|
37
|
+
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
38
|
+
import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
|
|
39
|
+
import { REFUND_CLAIMS_COLLECTION } from "../../../../features/orders/schemas/firestore.js";
|
|
40
|
+
import { reconcileDisputes } from "../../features/payment-cases/disputes.js";
|
|
41
|
+
import { refundableBase, committedRefundTotal, MONEY_EPSILON, } from "../../../shared/checkout/refundable-base.js";
|
|
42
|
+
/** How far back R2 looks. Two days of overlap, so a run that fails is covered by the next. */
|
|
43
|
+
const PAYMENT_WINDOW_HOURS = 48;
|
|
44
|
+
/** A refund claim unsettled for longer than this is a crash between call and record. */
|
|
45
|
+
const STALE_CLAIM_HOURS = 1;
|
|
46
|
+
/** Escalate a case this close to its external deadline. */
|
|
47
|
+
const DUE_SOON_HOURS = 72;
|
|
48
|
+
/* ───────────────────────── R1: events we received and lost ───────────────── */
|
|
49
|
+
/**
|
|
50
|
+
* Re-dispatch gateway events whose lease lapsed before they settled.
|
|
51
|
+
*
|
|
52
|
+
* 🛑 This catches "we received it and lost it" with a **5-minute** window
|
|
53
|
+
* instead of never. Before the inbox existed there was no artefact at all; the
|
|
54
|
+
* inbox alone makes the loss *visible*, and this is what makes it *recoverable*.
|
|
55
|
+
*
|
|
56
|
+
* Exported separately so `paymentWindowTimeout` can fold it in — a stuck
|
|
57
|
+
* payment event should not wait for a daily job.
|
|
58
|
+
*
|
|
59
|
+
* 🛑 It does NOT re-run the placement itself. Re-dispatching from a job would
|
|
60
|
+
* mean duplicating the webhook route's whole switch (and its RTDB signalling)
|
|
61
|
+
* in a second place, which is Root Cause #75's shape on the dispatch axis.
|
|
62
|
+
* Instead it RESETS the lease so the gateway's own redelivery — or an operator
|
|
63
|
+
* replaying from the admin page in Phase 8 — is accepted rather than
|
|
64
|
+
* short-circuited as a duplicate, and escalates to a case once the attempt
|
|
65
|
+
* budget is spent. The gateway retries for hours; this just stops the inbox
|
|
66
|
+
* from refusing the retry.
|
|
67
|
+
*/
|
|
68
|
+
export async function reconcileStuckGatewayEvents(ctx) {
|
|
69
|
+
let redispatched = 0;
|
|
70
|
+
let escalated = 0;
|
|
71
|
+
let stuck = [];
|
|
72
|
+
try {
|
|
73
|
+
stuck = await gatewayEventsRepository().listStuck(ctx.now, 20);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
void normalizeError(err);
|
|
77
|
+
ctx.logger.error("R1: stuck-event query failed", { err });
|
|
78
|
+
return { redispatched: 0, escalated: 0, reads: 0 };
|
|
79
|
+
}
|
|
80
|
+
for (const event of stuck) {
|
|
81
|
+
try {
|
|
82
|
+
if (event.attempts >= GATEWAY_EVENT_MAX_ATTEMPTS) {
|
|
83
|
+
await paymentCasesRepository().openOrObserve({
|
|
84
|
+
kind: "unprocessed_event",
|
|
85
|
+
subjectId: event.id,
|
|
86
|
+
severity: "error",
|
|
87
|
+
title: `Webhook never processed after ${event.attempts} attempts — ${event.rawEvent}`,
|
|
88
|
+
detail: [
|
|
89
|
+
`Gateway: ${event.gateway}`,
|
|
90
|
+
`Event: ${event.rawEvent} (${event.kind})`,
|
|
91
|
+
`Attempts: ${event.attempts}`,
|
|
92
|
+
event.gatewayOrderId ? `Gateway order: ${event.gatewayOrderId}` : null,
|
|
93
|
+
event.gatewayPaymentId ? `Gateway payment: ${event.gatewayPaymentId}` : null,
|
|
94
|
+
event.lastError ? `Last error: ${event.lastError}` : null,
|
|
95
|
+
"",
|
|
96
|
+
"The event body is preserved on the gatewayEvents row. If this is a",
|
|
97
|
+
"`paid` event, a buyer may have been charged with no order.",
|
|
98
|
+
]
|
|
99
|
+
.filter((l) => l !== null)
|
|
100
|
+
.join("\n"),
|
|
101
|
+
entityType: "event",
|
|
102
|
+
entityId: event.id,
|
|
103
|
+
...(typeof event.amount === "number" ? { amountAtRisk: event.amount } : {}),
|
|
104
|
+
gatewayEventIds: [event.id],
|
|
105
|
+
...(event.gatewayPaymentId ? { gatewayPaymentId: event.gatewayPaymentId } : {}),
|
|
106
|
+
now: ctx.now,
|
|
107
|
+
});
|
|
108
|
+
/*
|
|
109
|
+
* Settled `failed` + `terminal` so it leaves the stuck query. Without
|
|
110
|
+
* this the same row is re-examined every 5 minutes forever — which
|
|
111
|
+
* `openOrObserve` would dedupe into one case, but at the cost of a
|
|
112
|
+
* pointless read and write every tick for the life of the collection.
|
|
113
|
+
*/
|
|
114
|
+
await gatewayEventsRepository().settle(event.id, {
|
|
115
|
+
status: "failed",
|
|
116
|
+
terminal: true,
|
|
117
|
+
lastError: `attempt budget exhausted (${event.attempts})`,
|
|
118
|
+
now: ctx.now,
|
|
119
|
+
});
|
|
120
|
+
escalated += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
await gatewayEventsRepository().renewLease(event.id, event.attempts, ctx.now);
|
|
124
|
+
redispatched += 1;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
void normalizeError(err);
|
|
128
|
+
ctx.logger.error("R1: could not handle a stuck event — continuing", { id: event.id });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (stuck.length > 0) {
|
|
132
|
+
ctx.logger.warn("R1: stuck gateway events found", {
|
|
133
|
+
found: stuck.length,
|
|
134
|
+
redispatched,
|
|
135
|
+
escalated,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return { redispatched, escalated, reads: stuck.length };
|
|
139
|
+
}
|
|
140
|
+
/* ──────────────── R2: the gateway has it — do we? ───────────────────────── */
|
|
141
|
+
async function reconcileGatewayPayments(ctx) {
|
|
142
|
+
const payment = getProviders().payment;
|
|
143
|
+
let examined = 0;
|
|
144
|
+
let missing = 0;
|
|
145
|
+
let amountMismatch = 0;
|
|
146
|
+
let refundMismatch = 0;
|
|
147
|
+
let reads = 0;
|
|
148
|
+
if (!payment) {
|
|
149
|
+
ctx.logger.warn("R2: no payment provider registered — cannot reconcile");
|
|
150
|
+
return { examined, missing, amountMismatch, refundMismatch, reads };
|
|
151
|
+
}
|
|
152
|
+
const from = new Date(ctx.now.getTime() - PAYMENT_WINDOW_HOURS * 3600000);
|
|
153
|
+
let records;
|
|
154
|
+
try {
|
|
155
|
+
records = await payment.listPayments({ from, to: ctx.now, limit: 100 });
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
void normalizeError(err);
|
|
159
|
+
ctx.logger.error("R2: gateway payment listing failed", { err });
|
|
160
|
+
return { examined, missing, amountMismatch, refundMismatch, reads };
|
|
161
|
+
}
|
|
162
|
+
/*
|
|
163
|
+
* 🛑 An empty list means "NO INFORMATION" on the manual and PhonePe
|
|
164
|
+
* providers, which cannot enumerate payments at all — and concluding "nothing
|
|
165
|
+
* is missing" from a provider that cannot answer is the false-clean this
|
|
166
|
+
* whole job exists to avoid. So nothing is inferred from emptiness; the run
|
|
167
|
+
* simply records that it learned nothing.
|
|
168
|
+
*/
|
|
169
|
+
if (records.length === 0) {
|
|
170
|
+
ctx.logger.info("R2: provider returned no payments — no conclusion drawn", {
|
|
171
|
+
gateway: payment.name,
|
|
172
|
+
});
|
|
173
|
+
return { examined, missing, amountMismatch, refundMismatch, reads };
|
|
174
|
+
}
|
|
175
|
+
for (const record of records) {
|
|
176
|
+
/*
|
|
177
|
+
* Only CAPTURED payments. An `authorized`-but-uncaptured or `failed`
|
|
178
|
+
* payment having no order is correct, not a defect — and opening a case for
|
|
179
|
+
* every declined card would bury the real findings. Anything unrecognised
|
|
180
|
+
* is treated as not-captured, which fails toward silence.
|
|
181
|
+
*/
|
|
182
|
+
if (record.status !== "captured" && record.status !== "refunded")
|
|
183
|
+
continue;
|
|
184
|
+
examined += 1;
|
|
185
|
+
try {
|
|
186
|
+
/*
|
|
187
|
+
* Keyed on the gateway ORDER id, because that is what this codebase
|
|
188
|
+
* stores as `order.paymentId` — see the Razorpay provider's own class
|
|
189
|
+
* comment about keying every lookup on one id. Falls back to the payment
|
|
190
|
+
* id for gateways where they coincide.
|
|
191
|
+
*/
|
|
192
|
+
const lookupId = record.orderId ?? record.id;
|
|
193
|
+
const orders = await orderRepository.findByPaymentId(lookupId);
|
|
194
|
+
reads += 1;
|
|
195
|
+
if (orders.length === 0) {
|
|
196
|
+
missing += 1;
|
|
197
|
+
await paymentCasesRepository().openOrObserve({
|
|
198
|
+
kind: "missing_order",
|
|
199
|
+
subjectId: record.id,
|
|
200
|
+
severity: "error",
|
|
201
|
+
title: `Captured payment with no order — ${record.id}`,
|
|
202
|
+
detail: [
|
|
203
|
+
`🛑 The gateway says this payment was captured and we have no order for it.`,
|
|
204
|
+
`This is the 2026-10-21 shape: the buyer has been charged.`,
|
|
205
|
+
"",
|
|
206
|
+
`Gateway: ${record.gateway}`,
|
|
207
|
+
`Payment: ${record.id}`,
|
|
208
|
+
record.orderId ? `Gateway order: ${record.orderId}` : null,
|
|
209
|
+
`Amount: ${record.amount} ${record.currency}`,
|
|
210
|
+
record.capturedAt ? `Captured: ${record.capturedAt.toISOString()}` : null,
|
|
211
|
+
"",
|
|
212
|
+
`Check gatewayEvents for ${record.orderId ?? record.id} to see what the`,
|
|
213
|
+
`webhook did, and gatewayOrderClaims for a claim that was never released.`,
|
|
214
|
+
]
|
|
215
|
+
.filter((l) => l !== null)
|
|
216
|
+
.join("\n"),
|
|
217
|
+
entityType: "payment",
|
|
218
|
+
entityId: record.id,
|
|
219
|
+
amountAtRisk: record.amount,
|
|
220
|
+
gatewayPaymentId: record.id,
|
|
221
|
+
now: ctx.now,
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
for (const order of orders) {
|
|
226
|
+
/*
|
|
227
|
+
* Amount check. Compared against the order's own recorded total rather
|
|
228
|
+
* than recomputed from its lines — recomputing would re-derive the
|
|
229
|
+
* pricing pipeline here and disagree with it the day a rate changes,
|
|
230
|
+
* which is a sixth copy of the money rule (Root Cause #75).
|
|
231
|
+
*/
|
|
232
|
+
const recorded = order.totalPrice ?? 0;
|
|
233
|
+
if (Math.abs(recorded - record.amount) > MONEY_EPSILON) {
|
|
234
|
+
amountMismatch += 1;
|
|
235
|
+
await paymentCasesRepository().openOrObserve({
|
|
236
|
+
kind: "amount_mismatch",
|
|
237
|
+
subjectId: `${record.id}:${order.id}`,
|
|
238
|
+
severity: "error",
|
|
239
|
+
title: `Captured ${record.amount} but recorded ${recorded} — ${order.id}`,
|
|
240
|
+
detail: [
|
|
241
|
+
`The gateway captured a different amount from the one the order records.`,
|
|
242
|
+
`Either the buyer was over-charged or the order under-states what was paid.`,
|
|
243
|
+
"",
|
|
244
|
+
`Gateway: ${record.gateway}`,
|
|
245
|
+
`Payment: ${record.id}`,
|
|
246
|
+
`Order: ${order.id}`,
|
|
247
|
+
`Captured: ${record.amount} ${record.currency}`,
|
|
248
|
+
`Order total: ${recorded}`,
|
|
249
|
+
`Difference: ${(record.amount - recorded).toFixed(2)}`,
|
|
250
|
+
].join("\n"),
|
|
251
|
+
entityType: "order",
|
|
252
|
+
entityId: order.id,
|
|
253
|
+
amountAtRisk: Math.abs(record.amount - recorded),
|
|
254
|
+
orderIds: [order.id],
|
|
255
|
+
gatewayPaymentId: record.id,
|
|
256
|
+
now: ctx.now,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/*
|
|
260
|
+
* Refund check — the gateway's `amount_refunded` against our own
|
|
261
|
+
* non-failed refund events. `committedRefundTotal` is the same helper
|
|
262
|
+
* the refund cap uses, so "what we think we refunded" has ONE
|
|
263
|
+
* definition rather than a second one written here.
|
|
264
|
+
*/
|
|
265
|
+
const ours = committedRefundTotal(order);
|
|
266
|
+
if (Math.abs(ours - record.amountRefunded) > MONEY_EPSILON) {
|
|
267
|
+
refundMismatch += 1;
|
|
268
|
+
await paymentCasesRepository().openOrObserve({
|
|
269
|
+
kind: "refund_mismatch",
|
|
270
|
+
subjectId: `${record.id}:refunds`,
|
|
271
|
+
severity: record.amountRefunded > ours ? "error" : "warning",
|
|
272
|
+
title: `Refund total disagrees with the gateway — ${order.id}`,
|
|
273
|
+
detail: [
|
|
274
|
+
record.amountRefunded > ours
|
|
275
|
+
? `🛑 The gateway has refunded MORE than our records show. Money has left that we did not record.`
|
|
276
|
+
: `Our records show more refunded than the gateway did — a refund may have been reserved and never completed.`,
|
|
277
|
+
"",
|
|
278
|
+
`Gateway: ${record.gateway}`,
|
|
279
|
+
`Payment: ${record.id}`,
|
|
280
|
+
`Order: ${order.id}`,
|
|
281
|
+
`Gateway refunded: ${record.amountRefunded}`,
|
|
282
|
+
`Our committed refunds: ${ours}`,
|
|
283
|
+
`Refundable base: ${refundableBase(order)}`,
|
|
284
|
+
record.refundStatus ? `Gateway refund status: ${record.refundStatus}` : null,
|
|
285
|
+
]
|
|
286
|
+
.filter((l) => l !== null)
|
|
287
|
+
.join("\n"),
|
|
288
|
+
entityType: "order",
|
|
289
|
+
entityId: order.id,
|
|
290
|
+
amountAtRisk: Math.abs(record.amountRefunded - ours),
|
|
291
|
+
orderIds: [order.id],
|
|
292
|
+
gatewayPaymentId: record.id,
|
|
293
|
+
now: ctx.now,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
void normalizeError(err);
|
|
300
|
+
ctx.logger.error("R2: one payment failed to reconcile — continuing", {
|
|
301
|
+
paymentId: record.id,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return { examined, missing, amountMismatch, refundMismatch, reads };
|
|
306
|
+
}
|
|
307
|
+
/* ──────────────── R3: refund claimed, never settled ─────────────────────── */
|
|
308
|
+
/**
|
|
309
|
+
* A `refundClaims` row still `claimed` an hour later is a crash between
|
|
310
|
+
* "provider called" and "entry settled".
|
|
311
|
+
*
|
|
312
|
+
* 🛑 It is NOT resolved by guessing. The money may have left; the only
|
|
313
|
+
* authoritative answer is the gateway's, which a human reads. So this opens a
|
|
314
|
+
* case and stops — a sweep that decided for itself whether a refund had
|
|
315
|
+
* happened would be the double-refund generator the idempotency claim exists
|
|
316
|
+
* to prevent.
|
|
317
|
+
*/
|
|
318
|
+
async function reconcileStaleRefundClaims(ctx) {
|
|
319
|
+
const cutoff = new Date(ctx.now.getTime() - STALE_CLAIM_HOURS * 3600000);
|
|
320
|
+
let stale = 0;
|
|
321
|
+
try {
|
|
322
|
+
const snap = await ctx.db
|
|
323
|
+
.collection(REFUND_CLAIMS_COLLECTION)
|
|
324
|
+
.where("state", "==", "claimed")
|
|
325
|
+
.where("claimedAt", "<", cutoff)
|
|
326
|
+
.limit(50)
|
|
327
|
+
.get();
|
|
328
|
+
for (const doc of snap.docs) {
|
|
329
|
+
const data = doc.data();
|
|
330
|
+
stale += 1;
|
|
331
|
+
await paymentCasesRepository().openOrObserve({
|
|
332
|
+
kind: "refund_mismatch",
|
|
333
|
+
subjectId: `claim:${doc.id}`,
|
|
334
|
+
severity: "error",
|
|
335
|
+
title: `Refund claimed but never settled — ${doc.id}`,
|
|
336
|
+
detail: [
|
|
337
|
+
`A refund was claimed over ${STALE_CLAIM_HOURS}h ago and never recorded an outcome.`,
|
|
338
|
+
`That is a crash between calling the gateway and writing the result, so`,
|
|
339
|
+
`🛑 THE MONEY MAY HAVE LEFT. Read the gateway's refund list for this`,
|
|
340
|
+
`payment — do not assume either way, and do not retry the refund.`,
|
|
341
|
+
"",
|
|
342
|
+
`Claim: ${doc.id}`,
|
|
343
|
+
data.orderId ? `Order: ${data.orderId}` : null,
|
|
344
|
+
typeof data.amount === "number" ? `Amount: ${data.amount}` : null,
|
|
345
|
+
data.claimedBy ? `Claimed by: ${data.claimedBy}` : null,
|
|
346
|
+
]
|
|
347
|
+
.filter((l) => l !== null)
|
|
348
|
+
.join("\n"),
|
|
349
|
+
entityType: "refund",
|
|
350
|
+
entityId: doc.id,
|
|
351
|
+
...(typeof data.amount === "number" ? { amountAtRisk: data.amount } : {}),
|
|
352
|
+
...(data.orderId ? { orderIds: [data.orderId] } : {}),
|
|
353
|
+
now: ctx.now,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return { stale, reads: snap.size };
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
void normalizeError(err);
|
|
360
|
+
ctx.logger.error("R3: stale-claim query failed", { err });
|
|
361
|
+
return { stale, reads: 0 };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/* ──────────────── R5: a cancellation that still owes money ──────────────── */
|
|
365
|
+
/**
|
|
366
|
+
* A cancelled order whose recorded refund was never actually paid.
|
|
367
|
+
*
|
|
368
|
+
* ## 🛑 Why R3 cannot see this, which is the whole reason R5 exists
|
|
369
|
+
*
|
|
370
|
+
* R3 above watches `refundClaims` for a row stuck in `claimed` — i.e. a crash
|
|
371
|
+
* **after** `processRefundAction` took its idempotency claim. But
|
|
372
|
+
* `cancelOrderWithRefund` writes the cancellation and the refund *intent*
|
|
373
|
+
* first, and only then calls `processRefundAction`. So a failure in that window
|
|
374
|
+
* — the process dies, the settings read throws, the provider is unreachable
|
|
375
|
+
* before a claim is written — leaves **no `refundClaims` row at all**, and R3
|
|
376
|
+
* is structurally blind to it.
|
|
377
|
+
*
|
|
378
|
+
* What it does leave is an order that says, in its own fields, that it owes
|
|
379
|
+
* money: `status: "cancelled"` with a `refundAmount` and
|
|
380
|
+
* `refundStatus: "pending"`. That is the artefact, and it exists precisely
|
|
381
|
+
* because the intent is written before the money moves.
|
|
382
|
+
*
|
|
383
|
+
* ## Two equalities, no inequality, and the reason
|
|
384
|
+
*
|
|
385
|
+
* `where(status).where(refundStatus)` plus a `limit`. Deliberately **no
|
|
386
|
+
* `orderBy`**: `cancellationDate` and `refundAmount` are both optional fields,
|
|
387
|
+
* and an `orderBy` on an optional field silently excludes every document that
|
|
388
|
+
* lacks it (Root Cause #100) — which here would mean the sweep quietly skipping
|
|
389
|
+
* exactly the malformed rows it is looking for. With no `orderBy` Firestore
|
|
390
|
+
* orders by document id, which is stable and needs no second index.
|
|
391
|
+
*
|
|
392
|
+
* ## The settled check reads `refunds[]`, not the flag
|
|
393
|
+
*
|
|
394
|
+
* The flag says what was INTENDED; `refunds[]` says what HAPPENED, and it is
|
|
395
|
+
* maintained transactionally by reserve/settle. `settleRefund` now clears the
|
|
396
|
+
* flag to `"completed"` on a full processed refund, so a healthy refund leaves
|
|
397
|
+
* this query's result set on its own — but a crash between the provider call
|
|
398
|
+
* and that write would leave the flag standing with the money gone. Comparing
|
|
399
|
+
* against the committed total is what tells those two apart, and getting it
|
|
400
|
+
* backwards would report every successful refund as a failure.
|
|
401
|
+
*/
|
|
402
|
+
async function reconcileUnpaidCancellations(ctx) {
|
|
403
|
+
let owed = 0;
|
|
404
|
+
let stale = 0;
|
|
405
|
+
try {
|
|
406
|
+
const snap = await ctx.db
|
|
407
|
+
.collection("orders")
|
|
408
|
+
.where("status", "==", "cancelled")
|
|
409
|
+
.where("refundStatus", "==", "pending")
|
|
410
|
+
.limit(100)
|
|
411
|
+
.get();
|
|
412
|
+
/*
|
|
413
|
+
* Saturation is reported, never silent. A full page means there may be more
|
|
414
|
+
* behind it, and "we processed 100 and said nothing" reads identically to
|
|
415
|
+
* "there were exactly 100" — the `truncated` discipline.
|
|
416
|
+
*/
|
|
417
|
+
if (snap.size === 100) {
|
|
418
|
+
ctx.logger.warn("R5: hit the 100-order page limit — more unpaid cancellations may exist behind it");
|
|
419
|
+
}
|
|
420
|
+
for (const doc of snap.docs) {
|
|
421
|
+
const data = doc.data();
|
|
422
|
+
const intended = data.refundAmount ?? 0;
|
|
423
|
+
if (intended <= MONEY_EPSILON) {
|
|
424
|
+
/*
|
|
425
|
+
* `refundStatus: "pending"` with no amount. `cancelOrder` only writes
|
|
426
|
+
* the flag together with an amount, so this is a legacy row or a
|
|
427
|
+
* hand-edit — not money at risk, and not worth a case.
|
|
428
|
+
*/
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const committed = (data.refunds ?? [])
|
|
432
|
+
.filter((r) => r.status !== "failed")
|
|
433
|
+
.reduce((sum, r) => sum + (r.amount ?? 0), 0);
|
|
434
|
+
if (committed >= intended - MONEY_EPSILON) {
|
|
435
|
+
/*
|
|
436
|
+
* The money DID move; only the flag is stale — a crash between the
|
|
437
|
+
* provider call and `settleRefund`'s write, or a refund settled through
|
|
438
|
+
* a path that predates the flag being cleared. Repaired in place rather
|
|
439
|
+
* than escalated: there is nothing for a human to decide, and leaving
|
|
440
|
+
* it would make this order reappear in every future run.
|
|
441
|
+
*/
|
|
442
|
+
stale += 1;
|
|
443
|
+
await doc.ref
|
|
444
|
+
.update({ refundStatus: "completed", updatedAt: ctx.now })
|
|
445
|
+
.catch((err) => {
|
|
446
|
+
void normalizeError(err);
|
|
447
|
+
ctx.logger.warn("R5: could not clear a stale refundStatus", { orderId: doc.id });
|
|
448
|
+
});
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
owed += 1;
|
|
452
|
+
const shortfall = intended - committed;
|
|
453
|
+
await paymentCasesRepository().openOrObserve({
|
|
454
|
+
/*
|
|
455
|
+
* The SAME kind and the SAME `subjectId` that `cancelOrderWithRefund`
|
|
456
|
+
* uses when it opens this case synchronously — so when the failure was
|
|
457
|
+
* caught at the time, this run increments `observedCount` instead of
|
|
458
|
+
* opening a second case. That turns the count into "this has been owed
|
|
459
|
+
* for N days", which is the number an operator should be triaging by.
|
|
460
|
+
*/
|
|
461
|
+
kind: "manual_refund_due",
|
|
462
|
+
subjectId: doc.id,
|
|
463
|
+
severity: "error",
|
|
464
|
+
title: `Cancelled order still owes ₹${shortfall.toFixed(2)} — ${doc.id}`,
|
|
465
|
+
detail: [
|
|
466
|
+
"This order is cancelled and records a refund it has not paid.",
|
|
467
|
+
"",
|
|
468
|
+
"🛑 R3 cannot see this case: it watches `refundClaims`, and a failure",
|
|
469
|
+
"BEFORE a claim was taken leaves no claim row. The evidence is on the",
|
|
470
|
+
"order itself — a recorded `refundAmount` that `refunds[]` does not",
|
|
471
|
+
"cover.",
|
|
472
|
+
"",
|
|
473
|
+
`Order: ${doc.id}`,
|
|
474
|
+
`Recorded as owed: ₹${intended.toFixed(2)}`,
|
|
475
|
+
`Actually refunded: ₹${committed.toFixed(2)}`,
|
|
476
|
+
`Shortfall: ₹${shortfall.toFixed(2)}`,
|
|
477
|
+
data.paymentMethod ? `Payment method: ${data.paymentMethod}` : null,
|
|
478
|
+
data.paymentId ? `Gateway reference: ${data.paymentId}` : null,
|
|
479
|
+
data.userId ? `Buyer: ${data.userId}` : null,
|
|
480
|
+
data.cancellationReason ? `Cancelled because: ${data.cancellationReason}` : null,
|
|
481
|
+
"",
|
|
482
|
+
"Refunding from the admin order page is idempotent on this order, so a",
|
|
483
|
+
"retry cannot double-refund — if an earlier attempt did reach the",
|
|
484
|
+
"gateway, the retry returns that same refund.",
|
|
485
|
+
]
|
|
486
|
+
.filter((l) => l !== null)
|
|
487
|
+
.join("\n"),
|
|
488
|
+
entityType: "order",
|
|
489
|
+
entityId: doc.id,
|
|
490
|
+
amountAtRisk: shortfall,
|
|
491
|
+
orderIds: [doc.id],
|
|
492
|
+
...(data.paymentId ? { gatewayPaymentId: data.paymentId } : {}),
|
|
493
|
+
now: ctx.now,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return { owed, stale, reads: snap.size };
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
void normalizeError(err);
|
|
500
|
+
ctx.logger.error("R5: unpaid-cancellation query failed", { err });
|
|
501
|
+
return { owed, stale, reads: 0 };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/* ──────────────── R4: disputes, and deadlines about to pass ─────────────── */
|
|
505
|
+
async function reconcileDisputesAndDeadlines(ctx) {
|
|
506
|
+
let recorded = 0;
|
|
507
|
+
let escalated = 0;
|
|
508
|
+
let reads = 0;
|
|
509
|
+
const payment = getProviders().payment;
|
|
510
|
+
if (payment) {
|
|
511
|
+
try {
|
|
512
|
+
const disputes = await payment.listDisputes();
|
|
513
|
+
if (disputes.length > 0)
|
|
514
|
+
recorded = await reconcileDisputes(disputes);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
void normalizeError(err);
|
|
518
|
+
ctx.logger.error("R4: dispute listing failed", { err });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/*
|
|
522
|
+
* Deadline escalation. `listDueBefore` can only see cases that HAVE a
|
|
523
|
+
* `dueAt`, because a Firestore inequality excludes documents lacking the
|
|
524
|
+
* field (Root Cause #100) — correct here, since a case with no deadline
|
|
525
|
+
* cannot be nearly-overdue.
|
|
526
|
+
*
|
|
527
|
+
* Escalating to `error` is all this does. It does NOT send a notification per
|
|
528
|
+
* run: `openOrObserve`'s severity-only escalation plus the daily digest's
|
|
529
|
+
* "nearest external deadline" line is the alert, and a per-run email on a
|
|
530
|
+
* case that stays open for its whole 7-day response window would be seven
|
|
531
|
+
* emails against an 80/day budget for one fact.
|
|
532
|
+
*/
|
|
533
|
+
try {
|
|
534
|
+
const horizon = new Date(ctx.now.getTime() + DUE_SOON_HOURS * 3600000);
|
|
535
|
+
const due = await paymentCasesRepository().listDueBefore(horizon, 50);
|
|
536
|
+
reads += due.length;
|
|
537
|
+
for (const kase of due) {
|
|
538
|
+
if (kase.severity === "error")
|
|
539
|
+
continue;
|
|
540
|
+
await paymentCasesRepository().openOrObserve({
|
|
541
|
+
kind: kase.kind,
|
|
542
|
+
subjectId: kase.dedupeKey.slice(kase.kind.length + 1),
|
|
543
|
+
severity: "error",
|
|
544
|
+
title: kase.title,
|
|
545
|
+
detail: kase.detail,
|
|
546
|
+
entityType: kase.entityType,
|
|
547
|
+
entityId: kase.entityId,
|
|
548
|
+
...(kase.dueAt ? { dueAt: kase.dueAt } : {}),
|
|
549
|
+
...(typeof kase.amountAtRisk === "number"
|
|
550
|
+
? { amountAtRisk: kase.amountAtRisk }
|
|
551
|
+
: {}),
|
|
552
|
+
now: ctx.now,
|
|
553
|
+
});
|
|
554
|
+
escalated += 1;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
void normalizeError(err);
|
|
559
|
+
ctx.logger.error("R4: deadline escalation failed", { err });
|
|
560
|
+
}
|
|
561
|
+
return { recorded, escalated, reads };
|
|
562
|
+
}
|
|
563
|
+
/* ─────────────────────────────── the job ────────────────────────────────── */
|
|
564
|
+
export async function runPaymentReconcile(ctx) {
|
|
565
|
+
ctx.logger.info("Starting payment reconciliation (R2 + R3 + R4 + R5)");
|
|
566
|
+
/*
|
|
567
|
+
* 🛑 R1 is deliberately NOT called here. It belongs to
|
|
568
|
+
* `paymentWindowTimeout`, which runs every 5 minutes — a stuck payment event
|
|
569
|
+
* should not wait up to 24 hours. Running it in both places would double its
|
|
570
|
+
* writes for no extra coverage.
|
|
571
|
+
*/
|
|
572
|
+
const [payments, claims, disputes, cancellations] = await Promise.all([
|
|
573
|
+
reconcileGatewayPayments(ctx),
|
|
574
|
+
reconcileStaleRefundClaims(ctx),
|
|
575
|
+
reconcileDisputesAndDeadlines(ctx),
|
|
576
|
+
reconcileUnpaidCancellations(ctx),
|
|
577
|
+
]);
|
|
578
|
+
const result = {
|
|
579
|
+
stuckEventsRedispatched: 0,
|
|
580
|
+
stuckEventsEscalated: 0,
|
|
581
|
+
paymentsExamined: payments.examined,
|
|
582
|
+
missingOrders: payments.missing,
|
|
583
|
+
amountMismatches: payments.amountMismatch,
|
|
584
|
+
refundMismatches: payments.refundMismatch,
|
|
585
|
+
staleClaims: claims.stale,
|
|
586
|
+
disputesRecorded: disputes.recorded,
|
|
587
|
+
casesEscalated: disputes.escalated,
|
|
588
|
+
unpaidCancellations: cancellations.owed,
|
|
589
|
+
staleRefundFlagsCleared: cancellations.stale,
|
|
590
|
+
reads: payments.reads + claims.reads + disputes.reads + cancellations.reads,
|
|
591
|
+
};
|
|
592
|
+
/*
|
|
593
|
+
* Logged on EVERY run, including a clean one. The read count is the early
|
|
594
|
+
* warning for a regression to always-full scanning — which is exactly how
|
|
595
|
+
* the 2026-10-09 suspension and Root Cause #92's 12M invocations both became
|
|
596
|
+
* visible only on a billing page.
|
|
597
|
+
*/
|
|
598
|
+
ctx.logger.info("Payment reconciliation complete", { ...result });
|
|
599
|
+
if (result.missingOrders > 0) {
|
|
600
|
+
ctx.logger.error("🛑 Captured payments with no order", { count: result.missingOrders });
|
|
601
|
+
}
|
|
602
|
+
/*
|
|
603
|
+
* Loud, for the same reason as the line above: a buyer whose cancelled order
|
|
604
|
+
* never gave their money back has no way to tell, and nothing else in the
|
|
605
|
+
* system complains. Separated from `staleRefundFlagsCleared`, which is
|
|
606
|
+
* housekeeping and must not be reported in the same breath as money owed.
|
|
607
|
+
*/
|
|
608
|
+
if (result.unpaidCancellations > 0) {
|
|
609
|
+
ctx.logger.error("🛑 Cancelled orders still owing a refund", {
|
|
610
|
+
count: result.unpaidCancellations,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
@@ -5,6 +5,7 @@ import { restoreStockForOrder } from "../../features/checkout/stock-restore.js";
|
|
|
5
5
|
import { OrderStatusValues } from "../../../../features/orders/schemas/firestore.js";
|
|
6
6
|
import { PAYMENT_WINDOW_EXPIRED_REASON } from "../../../../features/orders/constants/payment-window.js";
|
|
7
7
|
import { ORDER_MESSAGES } from "../handlers/messages.js";
|
|
8
|
+
import { reconcileStuckGatewayEvents } from "./paymentReconcile.js";
|
|
8
9
|
/**
|
|
9
10
|
* 15-minute payment window sweep — cancels + restocks any `upi_manual`/
|
|
10
11
|
* `cash`/`emi` order still `pending`/`pending` with no proof uploaded past
|
|
@@ -19,6 +20,39 @@ import { ORDER_MESSAGES } from "../handlers/messages.js";
|
|
|
19
20
|
* (`paymentReviewAutoApprove`), not this one's.
|
|
20
21
|
*/
|
|
21
22
|
export async function runPaymentWindowTimeout(ctx) {
|
|
23
|
+
/*
|
|
24
|
+
* ── R1 rides along: gateway events we received and lost ───────────────────
|
|
25
|
+
*
|
|
26
|
+
* Folded into this sweep rather than given its own cron, for two reasons that
|
|
27
|
+
* both matter. Cloud Scheduler bills **per registered job**, so a fifth
|
|
28
|
+
* 5-minute function is a real recurring cost for one indexed read. And this
|
|
29
|
+
* job already runs at the cadence R1 needs: a webhook whose handler died
|
|
30
|
+
* mid-dispatch leaves a lapsed lease, and noticing that within ~5 minutes
|
|
31
|
+
* rather than up to 24 hours is the difference between the gateway's own
|
|
32
|
+
* retry still being in flight and a buyer having already emailed support.
|
|
33
|
+
*
|
|
34
|
+
* 🛑 FIRST, and outside the early return below. The original shape of this
|
|
35
|
+
* function returns as soon as there are no expired payment windows — which is
|
|
36
|
+
* most ticks — so appending R1 after that return would have made it run only
|
|
37
|
+
* on the minority of ticks that happened to have unrelated work. That is the
|
|
38
|
+
* sweep-coverage trap Root Cause #27 is about: three sweeps whose filters
|
|
39
|
+
* were each disjoint, with no proof any order was covered by one.
|
|
40
|
+
*
|
|
41
|
+
* It never throws: a reconciliation failure must not stop the payment-window
|
|
42
|
+
* cancellations this job exists for.
|
|
43
|
+
*/
|
|
44
|
+
try {
|
|
45
|
+
const r1 = await reconcileStuckGatewayEvents(ctx);
|
|
46
|
+
if (r1.redispatched > 0 || r1.escalated > 0) {
|
|
47
|
+
ctx.logger.warn("R1: recovered stuck gateway events", { ...r1 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
void normalizeError(err);
|
|
52
|
+
ctx.logger.error("R1 failed inside paymentWindowTimeout — continuing with the sweep", {
|
|
53
|
+
err,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
22
56
|
ctx.logger.info("Scanning orders past their 15-minute payment deadline");
|
|
23
57
|
const expired = await orderRepository.getExpiredPaymentDeadlines();
|
|
24
58
|
if (expired.length === 0) {
|