@mohasinac/appkit 4.59.0 → 4.60.1
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.d.ts +20 -2
- package/dist/_internal/server/features/orders/adapters.js +44 -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 +10 -4
- package/dist/client.js +29 -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 +43 -0
- package/dist/server.js +98 -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,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cancel an order AND give the money back — one owner, three callers.
|
|
3
|
+
*
|
|
4
|
+
* ## D14, and why passing the amount was only half of it
|
|
5
|
+
*
|
|
6
|
+
* `cancelOrder(orderId, reason, refundAmount?)` has carried an optional
|
|
7
|
+
* `refundAmount` since it was written, and **all four callers omitted it**. So
|
|
8
|
+
* cancelling a *paid* order restored stock and left the money with us, while
|
|
9
|
+
* `previewCancellationRefund` showed the buyer a figure nothing honoured.
|
|
10
|
+
*
|
|
11
|
+
* 🛑 **But passing the amount alone would have made it WORSE.** `cancelOrder`'s
|
|
12
|
+
* body writes `{ refundAmount, refundStatus: "pending" }` and nothing else —
|
|
13
|
+
* there is no provider call anywhere in it. So the obvious fix creates a *new*
|
|
14
|
+
* dead state: a recorded refund intent that nothing ever settles, which is
|
|
15
|
+
* strictly worse than today's honest silence because it reads, in the database,
|
|
16
|
+
* as though a refund is on its way. The amount and the movement have to land
|
|
17
|
+
* together, which is why this module exists rather than a one-line edit at each
|
|
18
|
+
* call site.
|
|
19
|
+
*
|
|
20
|
+
* ## The decision table
|
|
21
|
+
*
|
|
22
|
+
* Every row cancels the order and **restores its stock** — see `releaseStock`,
|
|
23
|
+
* which closes a second half of D14 the plan had recorded backwards. The table
|
|
24
|
+
* is only about the money.
|
|
25
|
+
*
|
|
26
|
+
* | condition | what happens to the money |
|
|
27
|
+
* |---|---|
|
|
28
|
+
* | `paymentStatus !== "paid"` | **nothing** — nothing was ever captured |
|
|
29
|
+
* | nothing left refundable | nothing — already refunded in full |
|
|
30
|
+
* | goods have left the seller | intent recorded, **case opened** — a human decides |
|
|
31
|
+
* | paid by cash / UPI-manual / EMI | intent recorded, **case opened** — there is no automatic rail |
|
|
32
|
+
* | paid through the gateway | **refunded automatically**, in full, once |
|
|
33
|
+
*
|
|
34
|
+
* The last row is the only automatic money movement, and it satisfies the
|
|
35
|
+
* plan's guardrail for one: the ceiling is an amount we captured, the recipient
|
|
36
|
+
* is the payer, and no counterparty is debited.
|
|
37
|
+
*
|
|
38
|
+
* ## Three things that are load-bearing
|
|
39
|
+
*
|
|
40
|
+
* **It goes through `processRefundAction`, never `payment.refund()` directly.**
|
|
41
|
+
* That action owns the idempotency claim, the cumulative cap against
|
|
42
|
+
* `refundableBase`, and the two-phase reserve/settle write around the provider
|
|
43
|
+
* call. A direct provider call here would reintroduce every defect step 8
|
|
44
|
+
* closed — D1, D2, D6 and D7 at once.
|
|
45
|
+
*
|
|
46
|
+
* **The key is `cancel_{orderId}`** — a NATURAL key, so a double-clicked Cancel
|
|
47
|
+
* button, a retried server action and a sweep all collapse to one refund. A
|
|
48
|
+
* `randomUUID()` here would be D2 wearing a new name.
|
|
49
|
+
*
|
|
50
|
+
* ## Points ARE reversed here now (Phase P)
|
|
51
|
+
*
|
|
52
|
+
* `pointsLedger`'s own header states the rule as a legal boundary: *"points
|
|
53
|
+
* spent on a cancelled order are returned as points"* — never as money, because
|
|
54
|
+
* paying them out in rupees is what would make this stored value, which in
|
|
55
|
+
* India is PPI territory with RBI authorisation attached.
|
|
56
|
+
*
|
|
57
|
+
* `reverseRedemptionForOrder` is that rule's first writer; the kind had been
|
|
58
|
+
* declared with none since it was written (D48). It is idempotent on the order
|
|
59
|
+
* — one reversal per order, checked by query before the write, because an
|
|
60
|
+
* append-only ledger cannot net off a double reversal.
|
|
61
|
+
*
|
|
62
|
+
* 🛑 It is a NO-OP today and still correct to call. No order has ever consumed
|
|
63
|
+
* points: the one code path that redeemed any was deleted on 2026-10-21
|
|
64
|
+
* because it debited the buyer without reducing the charge (D46 — see the note
|
|
65
|
+
* at its former site in `checkout/actions.ts`), and it had no caller, route
|
|
66
|
+
* field or UI tender before that. So the query finds no redemption and returns
|
|
67
|
+
* `{ reversed: 0 }`.
|
|
68
|
+
* Wiring it now rather than later is the cheap half — the expensive half is the
|
|
69
|
+
* ledger write, which is already keyed. When redemption becomes a real tender,
|
|
70
|
+
* cancellation already gives the points back instead of needing to remember to.
|
|
71
|
+
*
|
|
72
|
+
* **Cancel, then stock, then money.** The cancellation is the urgent half and
|
|
73
|
+
* must not be held hostage to a gateway round trip; the restock is a local
|
|
74
|
+
* write another buyer is actively waiting on; the refund is the one step that
|
|
75
|
+
* can hang for a full network timeout. The cost of that ordering is a window
|
|
76
|
+
* where the order is `cancelled` with the money still held — which is exactly
|
|
77
|
+
* why `refundAmount` is written *at cancellation time*, before the provider is
|
|
78
|
+
* called: a crash in the window leaves a detectable artefact rather than
|
|
79
|
+
* nothing, and R5 in `paymentReconcile` is the thing that finds it. The reverse
|
|
80
|
+
* ordering has no such consolation, because a refund posted against a live
|
|
81
|
+
* order that then fails to cancel is money gone from an order the buyer still
|
|
82
|
+
* expects to receive.
|
|
83
|
+
*/
|
|
84
|
+
import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
|
|
85
|
+
import { isManualPaymentMethod } from "../../../../features/orders/constants/payment-window.js";
|
|
86
|
+
import { MONEY_EPSILON } from "../../../shared/checkout/refundable-base.js";
|
|
87
|
+
/*
|
|
88
|
+
* 🛑 `refundCapacity` and NOT `totalPrice − platformFeeWithheld`, and the
|
|
89
|
+
* difference matters on a partially-refunded order: capacity subtracts what has
|
|
90
|
+
* already been committed, so cancelling an order that already had one line
|
|
91
|
+
* refunded returns the REMAINDER rather than the whole base a second time. The
|
|
92
|
+
* naive expression would over-refund by exactly the earlier refund, and
|
|
93
|
+
* `reserveRefund`'s transactional cap would then reject it — so it would
|
|
94
|
+
* surface as a confusing failure rather than a loss, but it would surface.
|
|
95
|
+
*
|
|
96
|
+
* Through `resolveRefundCapacity` rather than the pure function, so the
|
|
97
|
+
* settings read that legacy orders need has one owner shared with the sweep.
|
|
98
|
+
*/
|
|
99
|
+
import { resolveRefundCapacity } from "../refunds/capacity.js";
|
|
100
|
+
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
101
|
+
import { processRefundAction } from "../refunds/actions.js";
|
|
102
|
+
import { restoreStockForOrder } from "../checkout/stock-restore.js";
|
|
103
|
+
import { pointsRepository } from "../../../../features/points/repository/points.repository.js";
|
|
104
|
+
import { getAdminDb } from "../../../../providers/db-firebase/index.js";
|
|
105
|
+
import { classifyCancellation } from "./service.js";
|
|
106
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
107
|
+
import { serverLogger } from "../../../../monitoring/index.js";
|
|
108
|
+
/**
|
|
109
|
+
* Put the units back on the shelf.
|
|
110
|
+
*
|
|
111
|
+
* ## 🛑 A SECOND HALF OF D14 THAT THE PLAN RECORDED BACKWARDS
|
|
112
|
+
*
|
|
113
|
+
* The plan's D14 entry reads *"Stock is restored; **money is not** — including
|
|
114
|
+
* on a `paid` order."* Measured 2026-10-21: **neither is.**
|
|
115
|
+
* `restoreStockForOrder` has exactly four call sites —
|
|
116
|
+
* `paymentWindowTimeout`, `pendingOrderTimeout`,
|
|
117
|
+
* `adminRejectPaymentAsFraudAction`, and its own definition — and **not one of
|
|
118
|
+
* them is a user-initiated cancellation.** So a buyer, seller or admin
|
|
119
|
+
* cancelling an order left its units decremented, and the item stayed
|
|
120
|
+
* unavailable to every other buyer permanently. That is Root Cause #27's
|
|
121
|
+
* "holds stock forever" shape, reached by a different door.
|
|
122
|
+
*
|
|
123
|
+
* The plan's sentence is how the mistake survived: it describes the TIMEOUT
|
|
124
|
+
* jobs' behaviour and generalises it to all four callers, so "stock is fine,
|
|
125
|
+
* only money is broken" reads as settled and nobody re-checks.
|
|
126
|
+
*
|
|
127
|
+
* ## Why this is a separate call and not `extraOrderUpdate`
|
|
128
|
+
*
|
|
129
|
+
* `restoreStockForOrder` offers to write the caller's cancellation fields
|
|
130
|
+
* inside its own transaction, and its docstring explains why: atomicity, so
|
|
131
|
+
* the two "can never partially apply". Taking that offer would mean bypassing
|
|
132
|
+
* `orderRepository.cancelOrder` — and with it the status transition guard, the
|
|
133
|
+
* `statusHistory` entry and the obligation reconciliation, all of which live in
|
|
134
|
+
* `updateWithHistory`. Three mechanisms built on purpose, lost to gain
|
|
135
|
+
* atomicity on a pair of writes whose split failure is both detectable
|
|
136
|
+
* (`status: "cancelled"` with `stockRestored` unset) and repairable by calling
|
|
137
|
+
* this again.
|
|
138
|
+
*
|
|
139
|
+
* That repair is free because the function is **idempotent on its own flag** —
|
|
140
|
+
* `if (order.stockRestored) return` — which is independent of the order's
|
|
141
|
+
* status, so running it after the cancellation is fine.
|
|
142
|
+
*
|
|
143
|
+
* ## It never throws upward
|
|
144
|
+
*
|
|
145
|
+
* The cancellation has already been written and the buyer has already been told
|
|
146
|
+
* their order is cancelled. Failing the call now would report a failure for
|
|
147
|
+
* something that succeeded, and would leave the caller unsure whether to
|
|
148
|
+
* retry — while the money half still needs to run. Logged loudly instead.
|
|
149
|
+
*/
|
|
150
|
+
async function releaseStock(order) {
|
|
151
|
+
if (order.stockRestored)
|
|
152
|
+
return;
|
|
153
|
+
try {
|
|
154
|
+
await restoreStockForOrder(getAdminDb(), order, {});
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
void normalizeError(err);
|
|
158
|
+
serverLogger.error("cancellation: stock was NOT restored — these units are unavailable to other buyers until this is repaired", { orderId: order.id });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Give back any points this order consumed — `redemption_reversal`.
|
|
163
|
+
*
|
|
164
|
+
* 🛑 As POINTS, never as money. `pointsLedger`'s header states that as a legal
|
|
165
|
+
* boundary rather than a preference: cash-refunding them is what would make
|
|
166
|
+
* this stored value, and stored value in India is PPI territory with RBI
|
|
167
|
+
* authorisation attached.
|
|
168
|
+
*
|
|
169
|
+
* Idempotent on the order inside the repository, so calling it on every
|
|
170
|
+
* cancellation — including a re-run — is safe. Currently a no-op for every
|
|
171
|
+
* order, because nothing consumes points yet; wiring it now means cancellation
|
|
172
|
+
* will already be correct on the day redemption becomes a real tender, rather
|
|
173
|
+
* than needing somebody to remember.
|
|
174
|
+
*
|
|
175
|
+
* Never throws upward: the order is cancelled and the buyer has been told. A
|
|
176
|
+
* failed reversal is points the buyer should have back, so it is logged loudly
|
|
177
|
+
* and the balance is recoverable by an admin adjustment — whereas failing the
|
|
178
|
+
* cancellation here would leave them unable to retry something that already
|
|
179
|
+
* happened.
|
|
180
|
+
*/
|
|
181
|
+
async function returnPoints(order) {
|
|
182
|
+
try {
|
|
183
|
+
const { reversed } = await pointsRepository.reverseRedemptionForOrder(order.id, `order ${order.id} was cancelled`);
|
|
184
|
+
if (reversed > 0) {
|
|
185
|
+
serverLogger.info("cancellation: points returned to the buyer", {
|
|
186
|
+
orderId: order.id,
|
|
187
|
+
points: reversed,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
void normalizeError(err);
|
|
193
|
+
serverLogger.error("cancellation: points were NOT returned — the buyer is short by whatever they spent", { orderId: order.id });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Record that money is owed and nobody can move it automatically. */
|
|
197
|
+
async function openOwedCase(order, amount, why, detailLines) {
|
|
198
|
+
try {
|
|
199
|
+
const result = await paymentCasesRepository().openOrObserve({
|
|
200
|
+
kind: "manual_refund_due",
|
|
201
|
+
/*
|
|
202
|
+
* Keyed on the ORDER, not on the attempt. A retried cancellation, a
|
|
203
|
+
* sweep and an operator clicking twice are the same problem — one case,
|
|
204
|
+
* one notification. Including a timestamp here is what makes a queue
|
|
205
|
+
* unusable within a week.
|
|
206
|
+
*/
|
|
207
|
+
subjectId: order.id,
|
|
208
|
+
severity: "error",
|
|
209
|
+
title: `Refund of ₹${amount.toFixed(2)} owed on cancelled order ${order.id}`,
|
|
210
|
+
detail: [
|
|
211
|
+
...detailLines,
|
|
212
|
+
"",
|
|
213
|
+
`Order: ${order.id}`,
|
|
214
|
+
`Buyer: ${order.userId}`,
|
|
215
|
+
`Store: ${order.storeId ?? "—"}`,
|
|
216
|
+
`Payment method: ${order.paymentMethod ?? "unknown"}`,
|
|
217
|
+
`Gateway reference: ${order.paymentId ?? "—"}`,
|
|
218
|
+
`Amount owed: ₹${amount.toFixed(2)}`,
|
|
219
|
+
"",
|
|
220
|
+
"The order is cancelled and its stock is restored. `refundAmount` is recorded on the order with `refundStatus: \"pending\"`, so this case and the order agree about what is owed.",
|
|
221
|
+
].join("\n"),
|
|
222
|
+
entityType: "order",
|
|
223
|
+
entityId: order.id,
|
|
224
|
+
amountAtRisk: amount,
|
|
225
|
+
orderIds: [order.id],
|
|
226
|
+
...(order.paymentId ? { gatewayPaymentId: order.paymentId } : {}),
|
|
227
|
+
});
|
|
228
|
+
return result.id;
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
void normalizeError(err);
|
|
232
|
+
/*
|
|
233
|
+
* The case is the RECORD, not the obligation. Failing to write it must not
|
|
234
|
+
* fail the cancellation — the buyer's order is already cancelled and their
|
|
235
|
+
* stock released. Logged loudly, and the amount is still on the order, so
|
|
236
|
+
* D14's (c) detector finds it on the next pass regardless.
|
|
237
|
+
*/
|
|
238
|
+
serverLogger.error("cancellation refund: could not open the owed case", {
|
|
239
|
+
orderId: order.id,
|
|
240
|
+
amount,
|
|
241
|
+
why,
|
|
242
|
+
});
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Cancel an order and settle whatever money that owes.
|
|
248
|
+
*
|
|
249
|
+
* Callers pass the order they have already fetched and authorised. That is not
|
|
250
|
+
* only a saved read: the authorisation differs per caller (a buyer owns the
|
|
251
|
+
* order, a seller owns the store, an admin owns neither) and this function must
|
|
252
|
+
* not be the place that decides it.
|
|
253
|
+
*/
|
|
254
|
+
export async function cancelOrderWithRefund(order, opts) {
|
|
255
|
+
const ctx = {
|
|
256
|
+
actor: {
|
|
257
|
+
role: opts.actorRole,
|
|
258
|
+
...(opts.actorUid ? { uid: opts.actorUid } : {}),
|
|
259
|
+
},
|
|
260
|
+
reason: opts.reason,
|
|
261
|
+
...(opts.trigger ? { trigger: opts.trigger } : {}),
|
|
262
|
+
};
|
|
263
|
+
/*
|
|
264
|
+
* `refundedBy` on the other hand is a REQUIRED string on
|
|
265
|
+
* `ProcessRefundInput`, and a prefixed sentinel is the established shape
|
|
266
|
+
* there — `checkout/actions.ts` already passes
|
|
267
|
+
* `"system:checkout-auto-refund"`. Prefixed rather than bare so it can never
|
|
268
|
+
* be mistaken for a uid by something that only checks for a non-empty value.
|
|
269
|
+
*/
|
|
270
|
+
const refundedBy = opts.actorUid ?? `system:${opts.trigger ?? "cancellation"}`;
|
|
271
|
+
/*
|
|
272
|
+
* ── The unpaid case, and the asymmetry this comment exists to protect ─────
|
|
273
|
+
*
|
|
274
|
+
* 🛑 `paymentWindowTimeout` and `pendingOrderTimeout` call `cancelOrder` with
|
|
275
|
+
* NO amount, and that is CORRECT, not an oversight of the same kind D14
|
|
276
|
+
* describes: both sweeps exist precisely to cancel orders that were never
|
|
277
|
+
* paid. Someone reading D14 ("no cancel path passes a refund amount") will be
|
|
278
|
+
* tempted to make them uniform with the three user-initiated paths. Don't —
|
|
279
|
+
* a refund against a payment that never happened is not a no-op, it is a
|
|
280
|
+
* reserved slot against a `refundableBase` of zero, which `reserveRefund`
|
|
281
|
+
* refuses and which would make the sweeps start throwing.
|
|
282
|
+
*
|
|
283
|
+
* The guard lives here rather than at the three call sites so the rule has
|
|
284
|
+
* one statement, and so a fourth caller inherits it.
|
|
285
|
+
*/
|
|
286
|
+
if (order.paymentStatus !== "paid") {
|
|
287
|
+
const cancelled = await orderRepository.cancelOrder(order.id, opts.reason, undefined, ctx);
|
|
288
|
+
await releaseStock(order);
|
|
289
|
+
return { order: cancelled, refund: { moved: "none", why: "not-paid", amount: 0 } };
|
|
290
|
+
}
|
|
291
|
+
const capacity = await resolveRefundCapacity(order);
|
|
292
|
+
if (capacity.remaining <= MONEY_EPSILON) {
|
|
293
|
+
/*
|
|
294
|
+
* Paid, but already refunded in full — an order cancelled after its money
|
|
295
|
+
* was returned by another path. Nothing owed, and writing an amount of zero
|
|
296
|
+
* would set `refundStatus: "pending"` for a refund that will never come.
|
|
297
|
+
*/
|
|
298
|
+
const cancelled = await orderRepository.cancelOrder(order.id, opts.reason, undefined, ctx);
|
|
299
|
+
await releaseStock(order);
|
|
300
|
+
return {
|
|
301
|
+
order: cancelled,
|
|
302
|
+
refund: { moved: "none", why: "nothing-refundable", amount: 0 },
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const amount = capacity.remaining;
|
|
306
|
+
/*
|
|
307
|
+
* The intent, written in the same patch as the cancellation. This is what
|
|
308
|
+
* makes a crash in the window below detectable: `status: "cancelled"` with a
|
|
309
|
+
* `refundAmount` and no settled refund event is a provable, findable state.
|
|
310
|
+
*/
|
|
311
|
+
const cancelled = await orderRepository.cancelOrder(order.id, opts.reason, amount, ctx);
|
|
312
|
+
/*
|
|
313
|
+
* Stock before money. Restocking is a local write that another buyer is
|
|
314
|
+
* actively waiting on; the refund is a network call that can hang for its
|
|
315
|
+
* whole timeout. Doing the slow one first would hold the item hostage to the
|
|
316
|
+
* gateway's latency for no benefit.
|
|
317
|
+
*/
|
|
318
|
+
await releaseStock(order);
|
|
319
|
+
await returnPoints(order);
|
|
320
|
+
/*
|
|
321
|
+
* ── Goods have left the seller ────────────────────────────────────────────
|
|
322
|
+
*
|
|
323
|
+
* Asks the EXISTING classifier rather than re-testing `shippingDate`, so
|
|
324
|
+
* "has this been dispatched" has one definition shared with
|
|
325
|
+
* `assertOrderCancellable`. Re-deriving it here is how two copies of a rule
|
|
326
|
+
* start disagreeing (Root Cause #75) — and this particular rule is the one
|
|
327
|
+
* the plan calls the single highest-value line in the obligation design.
|
|
328
|
+
*
|
|
329
|
+
* Note the two buyer paths cannot reach this branch: `assertOrderCancellable`
|
|
330
|
+
* refuses a dispatched order outright ("request a return instead"), and
|
|
331
|
+
* `cancelOrderForUser`'s own status gate admits only pending/confirmed. It is
|
|
332
|
+
* reachable from the ADMIN route, which cancels any status the transition map
|
|
333
|
+
* allows — and `shipped → cancelled` is allowed.
|
|
334
|
+
*/
|
|
335
|
+
const route = classifyCancellation(order);
|
|
336
|
+
if (route.route !== "cancel") {
|
|
337
|
+
const caseId = await openOwedCase(cancelled, amount, "post-dispatch", [
|
|
338
|
+
"This order was cancelled AFTER the goods were dispatched, so the refund was not issued automatically.",
|
|
339
|
+
"",
|
|
340
|
+
"Who bears the outbound shipping leg is a decision about this order, not a rule: the parcel physically moved and somebody paid a courier for it. Decide the amount, then refund from the admin order page.",
|
|
341
|
+
]);
|
|
342
|
+
return {
|
|
343
|
+
order: cancelled,
|
|
344
|
+
refund: { moved: "owed", amount, why: "post-dispatch", ...(caseId ? { caseId } : {}) },
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/*
|
|
348
|
+
* ── No automatic rail exists ──────────────────────────────────────────────
|
|
349
|
+
*
|
|
350
|
+
* Cash, UPI-manual and EMI reach `paymentStatus: "paid"` through the manual
|
|
351
|
+
* payment review flow — an admin verified a screenshot. There is no gateway
|
|
352
|
+
* transaction to reverse, so the money goes back by bank transfer and a human
|
|
353
|
+
* records the UTR. Calling `processRefundAction` with `method: "manual"` here
|
|
354
|
+
* would append a refund event claiming the money was returned when nobody has
|
|
355
|
+
* sent it, which is the dead-state trap this module exists to avoid, one
|
|
356
|
+
* level in.
|
|
357
|
+
*
|
|
358
|
+
* The missing-reference case is folded in for the same reason: a gateway
|
|
359
|
+
* refund is order-keyed on `order.paymentId`, so without it there is nothing
|
|
360
|
+
* to call, and guessing is not an option.
|
|
361
|
+
*/
|
|
362
|
+
if (isManualPaymentMethod(order.paymentMethod)) {
|
|
363
|
+
const caseId = await openOwedCase(cancelled, amount, "manual-payment", [
|
|
364
|
+
`This order was paid by ${order.paymentMethod} — a manual method with no gateway transaction to reverse.`,
|
|
365
|
+
"",
|
|
366
|
+
"Transfer the amount to the buyer and record it from the admin order page, which captures the reference so the order and the bank statement agree.",
|
|
367
|
+
]);
|
|
368
|
+
return {
|
|
369
|
+
order: cancelled,
|
|
370
|
+
refund: { moved: "owed", amount, why: "manual-payment", ...(caseId ? { caseId } : {}) },
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (!order.paymentId) {
|
|
374
|
+
const caseId = await openOwedCase(cancelled, amount, "no-gateway-reference", [
|
|
375
|
+
"This order is marked paid but carries no gateway reference (`paymentId`), so an automatic refund has nothing to call.",
|
|
376
|
+
"",
|
|
377
|
+
"Find the payment in the gateway dashboard by amount and buyer, then refund from the admin order page.",
|
|
378
|
+
]);
|
|
379
|
+
return {
|
|
380
|
+
order: cancelled,
|
|
381
|
+
refund: { moved: "owed", amount, why: "no-gateway-reference", ...(caseId ? { caseId } : {}) },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/*
|
|
385
|
+
* ── The automatic path ────────────────────────────────────────────────────
|
|
386
|
+
*
|
|
387
|
+
* `type: "full"` because the amount IS the whole refundable base — the
|
|
388
|
+
* platform fee is withheld by policy, so "full" has never meant `totalPrice`
|
|
389
|
+
* (see `refundableBase`). On a partially-refunded order the remainder is
|
|
390
|
+
* still the rest of the base, so full is still the honest label.
|
|
391
|
+
*/
|
|
392
|
+
const result = await processRefundAction({
|
|
393
|
+
orderId: order.id,
|
|
394
|
+
type: "full",
|
|
395
|
+
amount,
|
|
396
|
+
/*
|
|
397
|
+
* `changed_mind` is the honest default: a cancellation before dispatch is a
|
|
398
|
+
* buyer deciding not to proceed with goods they never received. It is also
|
|
399
|
+
* the conservative choice, because it is NOT final-sale exempt — so a
|
|
400
|
+
* cancellation cannot be used to route around a final-sale listing's terms.
|
|
401
|
+
*
|
|
402
|
+
* 🛑 That gate cannot actually bite here, and it is worth knowing why
|
|
403
|
+
* rather than discovering it: `finalSale` is per-LINE and a pre-dispatch
|
|
404
|
+
* cancellation of a final-sale line would be refused by
|
|
405
|
+
* `processRefundAction`. If that is ever reported as a bug, the fix is a
|
|
406
|
+
* decision about whether final sale survives a pre-dispatch cancellation —
|
|
407
|
+
* not a quiet swap to an exempt reason code here.
|
|
408
|
+
*/
|
|
409
|
+
reasonCode: "changed_mind",
|
|
410
|
+
reasonNote: `Order cancelled — ${opts.reason}`,
|
|
411
|
+
/*
|
|
412
|
+
* The natural key. A double-clicked Cancel button, a retried server action
|
|
413
|
+
* and a future sweep all produce the same key, so the second one returns
|
|
414
|
+
* the first's refundId as a success instead of issuing a second refund.
|
|
415
|
+
*/
|
|
416
|
+
idempotencyKey: `cancel_${order.id}`,
|
|
417
|
+
method: "phonepe",
|
|
418
|
+
phonepeOrderId: order.paymentId,
|
|
419
|
+
confirmIrrevocable: true,
|
|
420
|
+
refundedBy,
|
|
421
|
+
});
|
|
422
|
+
/*
|
|
423
|
+
* 🛑 UNWRAP THE ENVELOPE. `processRefundAction` is a `"use server"` action, so
|
|
424
|
+
* it returns `ActionResult<T>` — `{ ok, data }`, never the payload. Treating
|
|
425
|
+
* the envelope as the result is Root Cause #98, which cost nine seller edit
|
|
426
|
+
* pages a blank form and a destructive save: an envelope is always truthy, so
|
|
427
|
+
* `if (!result)` can never fire and `result.refundId` is silently `undefined`.
|
|
428
|
+
*/
|
|
429
|
+
if (!result.ok) {
|
|
430
|
+
const caseId = await openOwedCase(cancelled, amount, "gateway-failed", [
|
|
431
|
+
"The order was cancelled and its stock restored, but the automatic gateway refund FAILED.",
|
|
432
|
+
"",
|
|
433
|
+
`Gateway error: ${result.error ?? "unknown"}`,
|
|
434
|
+
"",
|
|
435
|
+
"Retry the refund from the admin order page. It is idempotent on this order, so a retry cannot double-refund — if the first attempt did reach the gateway, the retry returns that same refund.",
|
|
436
|
+
]);
|
|
437
|
+
serverLogger.error("cancellation refund: gateway refund failed — money still held", {
|
|
438
|
+
orderId: order.id,
|
|
439
|
+
amount,
|
|
440
|
+
caseId,
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
order: cancelled,
|
|
444
|
+
refund: { moved: "owed", amount, why: "gateway-failed", ...(caseId ? { caseId } : {}) },
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
serverLogger.info("cancellation refunded", {
|
|
448
|
+
orderId: order.id,
|
|
449
|
+
amount,
|
|
450
|
+
withheld: capacity.withheld,
|
|
451
|
+
refundId: result.data.refundId,
|
|
452
|
+
actorRole: opts.actorRole,
|
|
453
|
+
});
|
|
454
|
+
/*
|
|
455
|
+
* Re-read, because `processRefundAction` wrote to the order twice after our
|
|
456
|
+
* `cancelOrder` returned (reserve, then settle) — so the document we hold is
|
|
457
|
+
* already stale, and handing a caller a stale order whose `refunds[]` is
|
|
458
|
+
* empty would make every surface downstream show "no refund yet" on an order
|
|
459
|
+
* that was just refunded.
|
|
460
|
+
*/
|
|
461
|
+
const settled = await orderRepository.findById(order.id);
|
|
462
|
+
return {
|
|
463
|
+
order: settled ?? cancelled,
|
|
464
|
+
refund: { moved: "gateway", amount, refundId: result.data.refundId },
|
|
465
|
+
};
|
|
466
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { getOrderForDetail, getOrdersForBuyer, getRecentOrdersForBuyer } from "./data";
|
|
2
2
|
export { orderDocumentToOrder } from "./adapters";
|
|
3
|
-
export { assertOrderOwnership, assertOrderCancellable } from "./service";
|
|
3
|
+
export { assertOrderOwnership, assertOrderCancellable, classifyCancellation } from "./service";
|
|
4
|
+
export type { CancellationRoute } from "./service";
|
|
4
5
|
export { createOrderAction, cancelOrderAction, requestReturnAction, updateOrderStatusAction } from "./actions";
|
|
5
6
|
export { ORDERS_PAGE_SIZE, ORDER_CANCELLABLE_STATUSES, ORDER_RETURN_WINDOW_DAYS } from "../../../shared/features/orders/config";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { getOrderForDetail, getOrdersForBuyer, getRecentOrdersForBuyer } from "./data.js";
|
|
2
2
|
export { orderDocumentToOrder } from "./adapters.js";
|
|
3
|
-
export { assertOrderOwnership, assertOrderCancellable } from "./service.js";
|
|
3
|
+
export { assertOrderOwnership, assertOrderCancellable, classifyCancellation } from "./service.js";
|
|
4
4
|
export { createOrderAction, cancelOrderAction, requestReturnAction, updateOrderStatusAction } from "./actions.js";
|
|
5
5
|
export { ORDERS_PAGE_SIZE, ORDER_CANCELLABLE_STATUSES, ORDER_RETURN_WINDOW_DAYS } from "../../../shared/features/orders/config.js";
|
|
@@ -1,4 +1,64 @@
|
|
|
1
1
|
import type { OrderDocument } from "../../../../features/orders/schemas";
|
|
2
2
|
export declare function assertOrderOwnership(orderId: string, userId: string): Promise<OrderDocument>;
|
|
3
|
+
/**
|
|
4
|
+
* What a buyer asking to cancel is actually asking for.
|
|
5
|
+
*
|
|
6
|
+
* 🛑 **`shippingDate != null` IS THE DISCRIMINATOR, not the status set**, and
|
|
7
|
+
* the plan calls codifying this the single highest-value line in the obligation
|
|
8
|
+
* design. The reason is that "cancel before dispatch" and "cancel after
|
|
9
|
+
* dispatch" are different EVENTS with different money:
|
|
10
|
+
*
|
|
11
|
+
* | | before dispatch | after dispatch |
|
|
12
|
+
* |---|---|---|
|
|
13
|
+
* | what happens | cancel, restock, full refund | refuse the cancel — convert to a return/RTO |
|
|
14
|
+
* | who bears shipping | nobody; it is refunded | the buyer bears the outbound leg |
|
|
15
|
+
* | when money moves | immediately | on receipt of the goods |
|
|
16
|
+
*
|
|
17
|
+
* The status set cannot express that, because `processing` covers both "paid,
|
|
18
|
+
* sitting in a queue" and "packed, courier collecting this afternoon" — while
|
|
19
|
+
* `shippingDate` is the fact that the parcel physically left.
|
|
20
|
+
*
|
|
21
|
+
* And the old behaviour was a dead end: `ORDER_CANCELLABLE_STATUSES` is
|
|
22
|
+
* `["pending", "confirmed"]`, so a buyer whose order was `processing` — or
|
|
23
|
+
* shipped an hour ago — got a flat *"Only pending or confirmed orders can be
|
|
24
|
+
* cancelled"* and **no alternative offered**. That is the Root Cause #56 shape
|
|
25
|
+
* (a surface that refuses without routing anywhere) applied to money.
|
|
26
|
+
*/
|
|
27
|
+
export type CancellationRoute = {
|
|
28
|
+
route: "cancel";
|
|
29
|
+
reason: "not yet dispatched";
|
|
30
|
+
} | {
|
|
31
|
+
route: "return";
|
|
32
|
+
reason: "already dispatched";
|
|
33
|
+
dispatchedAt: Date;
|
|
34
|
+
} | {
|
|
35
|
+
route: "refused";
|
|
36
|
+
reason: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function classifyCancellation(order: OrderDocument): CancellationRoute;
|
|
3
39
|
export declare function assertOrderCancellable(orderId: string, userId: string): Promise<OrderDocument>;
|
|
40
|
+
/**
|
|
41
|
+
* Is the buyer's return window still open?
|
|
42
|
+
*
|
|
43
|
+
* 🛑 The window is measured from DELIVERY, and until 2026-10-21 nothing in the
|
|
44
|
+
* application wrote `deliveryDate` at all — only seed data and display
|
|
45
|
+
* components referenced it — so this function always took its fallback branch
|
|
46
|
+
* and measured from **`updatedAt`**, i.e. from the last time anyone touched
|
|
47
|
+
* the order. A seller adding a note, an admin assigning a worker, or any
|
|
48
|
+
* `statusHistory` append silently restarted the buyer's 7 days. The anchor is
|
|
49
|
+
* now stamped by `OrderRepository.updateWithHistory` on the transition into
|
|
50
|
+
* `delivered`, so the primary branch is reachable for the first time.
|
|
51
|
+
*
|
|
52
|
+
* Two deliberate choices in the fallback:
|
|
53
|
+
*
|
|
54
|
+
* · **It still exists**, because every order delivered before that date has
|
|
55
|
+
* no anchor, and the alternative — refusing the return outright — would
|
|
56
|
+
* punish buyers for our omission.
|
|
57
|
+
* · **It fails OPEN** (`?? Date.now()` keeps the window open rather than
|
|
58
|
+
* closing it). For a *buyer's* right the safe direction is to allow; this
|
|
59
|
+
* is the mirror of the payout hold, where the same uncertainty must
|
|
60
|
+
* WITHHOLD, because there the unsafe direction is releasing money we
|
|
61
|
+
* cannot get back. Same missing anchor, opposite correct defaults — which
|
|
62
|
+
* is why the two are not sharing one helper.
|
|
63
|
+
*/
|
|
4
64
|
export declare function assertReturnWindowOpen(orderId: string, userId: string): Promise<OrderDocument>;
|
|
@@ -9,18 +9,92 @@ export async function assertOrderOwnership(orderId, userId) {
|
|
|
9
9
|
throw new OrderOwnershipError(orderId);
|
|
10
10
|
return order;
|
|
11
11
|
}
|
|
12
|
+
export function classifyCancellation(order) {
|
|
13
|
+
/*
|
|
14
|
+
* Terminal first. A cancelled or refunded order is not a cancellation
|
|
15
|
+
* request, it is a duplicate — and answering "convert this to a return"
|
|
16
|
+
* would send the buyer into a flow for goods they no longer have.
|
|
17
|
+
*/
|
|
18
|
+
if (order.status === "cancelled" || order.status === "refunded") {
|
|
19
|
+
return { route: "refused", reason: `this order is already ${order.status}` };
|
|
20
|
+
}
|
|
21
|
+
/*
|
|
22
|
+
* A return already in flight is its own conversation. Offering a cancellation
|
|
23
|
+
* on top would give the buyer two parallel money paths for one order, which
|
|
24
|
+
* is how a double refund starts.
|
|
25
|
+
*/
|
|
26
|
+
if (order.status === "return_requested" || order.status === "returned") {
|
|
27
|
+
return { route: "refused", reason: "a return is already in progress for this order" };
|
|
28
|
+
}
|
|
29
|
+
if (order.shippingDate) {
|
|
30
|
+
return {
|
|
31
|
+
route: "return",
|
|
32
|
+
reason: "already dispatched",
|
|
33
|
+
dispatchedAt: order.shippingDate,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { route: "cancel", reason: "not yet dispatched" };
|
|
37
|
+
}
|
|
12
38
|
export async function assertOrderCancellable(orderId, userId) {
|
|
13
39
|
const order = await assertOrderOwnership(orderId, userId);
|
|
40
|
+
/*
|
|
41
|
+
* 🛑 The classifier decides, and `ORDER_CANCELLABLE_STATUSES` is now only a
|
|
42
|
+
* BACKSTOP below rather than the rule.
|
|
43
|
+
*
|
|
44
|
+
* Kept in that order deliberately: the classifier widens what is cancellable
|
|
45
|
+
* (a `processing` order that has not shipped now qualifies, correctly), and
|
|
46
|
+
* the status check behind it still refuses anything the classifier admitted
|
|
47
|
+
* but the lifecycle does not recognise. Replacing the status check outright
|
|
48
|
+
* would have been a wider behaviour change than the evidence supports.
|
|
49
|
+
*/
|
|
50
|
+
const route = classifyCancellation(order);
|
|
51
|
+
if (route.route === "return") {
|
|
52
|
+
throw new OrderCancelError(`${order.status} (dispatched ${route.dispatchedAt.toISOString()} — request a return instead)`);
|
|
53
|
+
}
|
|
54
|
+
if (route.route === "refused") {
|
|
55
|
+
throw new OrderCancelError(`${order.status} (${route.reason})`);
|
|
56
|
+
}
|
|
14
57
|
const cancellable = ORDER_CANCELLABLE_STATUSES;
|
|
15
58
|
if (!cancellable.includes(order.status)) {
|
|
16
59
|
throw new OrderCancelError(order.status);
|
|
17
60
|
}
|
|
18
61
|
return order;
|
|
19
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Is the buyer's return window still open?
|
|
65
|
+
*
|
|
66
|
+
* 🛑 The window is measured from DELIVERY, and until 2026-10-21 nothing in the
|
|
67
|
+
* application wrote `deliveryDate` at all — only seed data and display
|
|
68
|
+
* components referenced it — so this function always took its fallback branch
|
|
69
|
+
* and measured from **`updatedAt`**, i.e. from the last time anyone touched
|
|
70
|
+
* the order. A seller adding a note, an admin assigning a worker, or any
|
|
71
|
+
* `statusHistory` append silently restarted the buyer's 7 days. The anchor is
|
|
72
|
+
* now stamped by `OrderRepository.updateWithHistory` on the transition into
|
|
73
|
+
* `delivered`, so the primary branch is reachable for the first time.
|
|
74
|
+
*
|
|
75
|
+
* Two deliberate choices in the fallback:
|
|
76
|
+
*
|
|
77
|
+
* · **It still exists**, because every order delivered before that date has
|
|
78
|
+
* no anchor, and the alternative — refusing the return outright — would
|
|
79
|
+
* punish buyers for our omission.
|
|
80
|
+
* · **It fails OPEN** (`?? Date.now()` keeps the window open rather than
|
|
81
|
+
* closing it). For a *buyer's* right the safe direction is to allow; this
|
|
82
|
+
* is the mirror of the payout hold, where the same uncertainty must
|
|
83
|
+
* WITHHOLD, because there the unsafe direction is releasing money we
|
|
84
|
+
* cannot get back. Same missing anchor, opposite correct defaults — which
|
|
85
|
+
* is why the two are not sharing one helper.
|
|
86
|
+
*/
|
|
20
87
|
export async function assertReturnWindowOpen(orderId, userId) {
|
|
21
88
|
const order = await assertOrderOwnership(orderId, userId);
|
|
22
89
|
if (order.status !== "delivered")
|
|
23
90
|
throw new OrderCancelError(order.status);
|
|
91
|
+
/*
|
|
92
|
+
* `BaseRepository.mapDoc` runs `deserializeTimestamps`, so a Firestore
|
|
93
|
+
* Timestamp has already become a real `Date` by the time it reaches here —
|
|
94
|
+
* which is what makes `instanceof Date` a valid test on THIS path. It would
|
|
95
|
+
* not be on a raw `d.data()` read (see `toDateOrNull` in the repository,
|
|
96
|
+
* which handles all four shapes for exactly that reason).
|
|
97
|
+
*/
|
|
24
98
|
const deliveredAt = order.deliveryDate instanceof Date
|
|
25
99
|
? order.deliveryDate
|
|
26
100
|
: new Date(order.updatedAt ?? Date.now());
|