@mohasinac/appkit 4.61.0 → 4.62.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/lottery/LotteryAdminEditView.js +19 -3
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
- package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
- package/dist/_internal/server/features/checkout/actions.js +542 -26
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +33 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +239 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +15 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +51 -0
- package/dist/_internal/server/functions/scheduled.js +38 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
- package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.js +177 -9
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
- package/dist/_internal/shared/checkout/order-math.js +173 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/split.d.ts +87 -0
- package/dist/_internal/shared/checkout/split.js +100 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +88 -0
- package/dist/_internal/shared/content/published-values.js +198 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
- package/dist/_internal/shared/features/admin/integrations.js +253 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
- package/dist/_internal/shared/features/points/accepts.js +43 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +12 -1
- package/dist/client.js +31 -1
- package/dist/constants/api-endpoints.d.ts +45 -0
- package/dist/constants/api-endpoints.js +15 -0
- package/dist/constants/field-names.d.ts +18 -0
- package/dist/constants/field-names.js +28 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/core/server-action.js +19 -3
- package/dist/errors/error-mapping.js +26 -1
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminActionIndexView.js +100 -5
- package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselView.js +96 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminMediaView.js +36 -3
- package/dist/features/admin/components/AdminNavigationView.js +4 -1
- package/dist/features/admin/components/AdminOffersView.js +2 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +67 -5
- package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
- package/dist/features/admin/components/AdminProductEditorView.js +2 -2
- package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
- package/dist/features/admin/components/AdminSectionsView.js +486 -224
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
- package/dist/features/admin/repository/notification.repository.d.ts +15 -0
- package/dist/features/admin/repository/notification.repository.js +32 -0
- package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +30 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
- package/dist/features/audit-log/schemas/firestore.js +15 -0
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +19 -0
- package/dist/features/cart/repository/cart.repository.js +27 -0
- package/dist/features/cart/schemas/firestore.d.ts +92 -0
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/events/components/AdminEventEditorView.js +8 -6
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
- package/dist/features/orders/components/RefundRequestView.js +22 -4
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +57 -0
- package/dist/features/orders/repository/orders.repository.js +145 -0
- package/dist/features/orders/schemas/firestore.d.ts +196 -1
- package/dist/features/orders/schemas/index.d.ts +60 -12
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +26 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/seller-actions.js +33 -3
- package/dist/features/seller/components/FulfillmentView.js +3 -2
- package/dist/features/seller/components/SellerAddressesView.js +32 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
- package/dist/features/seller/components/SellerBundlesView.js +53 -14
- package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
- package/dist/features/seller/components/SellerListingTypeView.js +56 -10
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
- package/dist/features/seller/components/SellerProductsView.js +5 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
- package/dist/features/seller/components/SellerShippingView.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
- package/dist/features/seller/components/SellerSupportView.js +69 -2
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shell/SectionForm.js +20 -16
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
- package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/schemas/firestore.d.ts +71 -0
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/schemas/index.d.ts +6 -6
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +17 -2
- package/dist/next/routing/route-map.d.ts +24 -0
- package/dist/next/routing/route-map.js +19 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/schemas/registry.d.ts +51 -22
- package/dist/schemas/sieve.d.ts +5 -5
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +11 -2
- package/dist/server.js +40 -2
- package/dist/styles.css +64 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +43 -9
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/ui/forms/FormErrorList.d.ts +0 -16
- package/dist/ui/forms/FormErrorList.js +34 -1
- package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
- package/dist/ui/forms/apply-zod-issues.js +23 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/rich-text/RichText.style.css +48 -79
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1831 -1822
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integration switches, and what turning each one OFF actually does.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 This is a VIEW over existing fields, never a second store
|
|
5
|
+
*
|
|
6
|
+
* Three of the four already have an authoritative field:
|
|
7
|
+
*
|
|
8
|
+
* payments -> `payment.razorpayEnabled`
|
|
9
|
+
* email -> `messaging.emailEnabled` (read by `guardSend`)
|
|
10
|
+
* shipping -> `shipping.provider` ("manual" | "shiprocket")
|
|
11
|
+
*
|
|
12
|
+
* Adding an `integrationSwitches.payments` beside those would be a
|
|
13
|
+
* denormalised mirror, and it would drift the first time a path wrote one of
|
|
14
|
+
* the pair and not the other (Root Cause #42) — at which point the admin page
|
|
15
|
+
* says "on" while the code that enforces it reads "off". Only `support.enabled`
|
|
16
|
+
* is new, because only support had nothing.
|
|
17
|
+
*
|
|
18
|
+
* ## 🛑 Off does not mean the same thing twice
|
|
19
|
+
*
|
|
20
|
+
* "Payments off" stops money. "Support off" hides a contact route. "Shipping
|
|
21
|
+
* off" is not even expressible — a parcel still has to move, so the shipping
|
|
22
|
+
* switch selects manual (self-ship) rather than stopping anything.
|
|
23
|
+
*
|
|
24
|
+
* Presenting four interchangeable toggles would invite an admin to flip the
|
|
25
|
+
* expensive one by muscle memory, so every row carries its own
|
|
26
|
+
* `offConsequence` and the dangerous ones are marked.
|
|
27
|
+
*/
|
|
28
|
+
import type { SiteSettingsDocument } from "../../../../features/admin/schemas/firestore";
|
|
29
|
+
export type IntegrationId = "payments" | "email" | "shipping" | "support";
|
|
30
|
+
export interface IntegrationDescriptor {
|
|
31
|
+
id: IntegrationId;
|
|
32
|
+
label: string;
|
|
33
|
+
/** One line: what this integration IS. */
|
|
34
|
+
summary: string;
|
|
35
|
+
/** 🛑 Shown on the row. What switching it off does, in the admin's terms. */
|
|
36
|
+
offConsequence: string;
|
|
37
|
+
/**
|
|
38
|
+
* `true` when turning it off stops money or breaks a flow a user is
|
|
39
|
+
* mid-way through. Drives a confirmation and a visual warning — an
|
|
40
|
+
* accidental click here is expensive.
|
|
41
|
+
*/
|
|
42
|
+
severe: boolean;
|
|
43
|
+
/** The settings path the switch actually writes. Named, so it is auditable. */
|
|
44
|
+
field: string;
|
|
45
|
+
/**
|
|
46
|
+
* Whether this integration's API routes are closed when it is off, and WHY
|
|
47
|
+
* that answer is what it is. Three different answers for four integrations,
|
|
48
|
+
* so a uniform wrapper applied to everything would be wrong three ways.
|
|
49
|
+
*
|
|
50
|
+
* - `"gated"` — the routes 404 when off (payments, support).
|
|
51
|
+
* - `"none"` — there is nothing to gate, stated with the reason, so nobody
|
|
52
|
+
* later "completes" the set by guarding a route that must
|
|
53
|
+
* stay open.
|
|
54
|
+
*/
|
|
55
|
+
routeGuard: "gated" | "none";
|
|
56
|
+
/** Human reason for `routeGuard`, shown nowhere — it is for the next reader. */
|
|
57
|
+
routeGuardNote: string;
|
|
58
|
+
/**
|
|
59
|
+
* 🛑 Which way the guard fails when the settings read itself throws, and this
|
|
60
|
+
* genuinely DIFFERS per integration — a single uniform direction would be
|
|
61
|
+
* the wrong call for one of them whichever one was picked.
|
|
62
|
+
*
|
|
63
|
+
* `true` (payments): a Firestore blip must not open a money route. Closed is
|
|
64
|
+
* the safe direction for money.
|
|
65
|
+
*
|
|
66
|
+
* `false` (support): refusing support because Firestore hiccupped is worse
|
|
67
|
+
* than briefly allowing a ticket nobody wanted — and it is the same choice
|
|
68
|
+
* `guardSend` makes on the same read.
|
|
69
|
+
*/
|
|
70
|
+
failClosed: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 🛑 A `Record<IntegrationId, …>`, so a fifth integration cannot be added
|
|
74
|
+
* without writing its off-consequence. A row with no stated consequence is
|
|
75
|
+
* exactly the interchangeable-toggle failure this file exists to prevent.
|
|
76
|
+
*/
|
|
77
|
+
export declare const INTEGRATIONS: Record<IntegrationId, IntegrationDescriptor>;
|
|
78
|
+
/** Display order. Money first, because it is what an admin came to check. */
|
|
79
|
+
export declare const INTEGRATION_ORDER: readonly IntegrationId[];
|
|
80
|
+
/** What a switch reads/writes. `shipping` is a provider, not a boolean. */
|
|
81
|
+
export type IntegrationState = {
|
|
82
|
+
enabled: boolean;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Read each switch out of a settings document.
|
|
86
|
+
*
|
|
87
|
+
* 🛑 Defaults differ per integration and are NOT a uniform `?? false`:
|
|
88
|
+
* `razorpayEnabled` genuinely defaults off (it is opt-in and the account runs
|
|
89
|
+
* test keys), while email and support default ON because an older settings
|
|
90
|
+
* document with no such field must not read as "we stopped talking to
|
|
91
|
+
* customers".
|
|
92
|
+
*
|
|
93
|
+
* 🛑 THIS FUNCTION IS THE EXPRESSION THE ROUTE GUARDS USE. `withOnlinePaymentEnabled`
|
|
94
|
+
* had hand-written the payments half, and the first version of this file
|
|
95
|
+
* reproduced it WRONG — see the payments line. Both now resolve here, so the
|
|
96
|
+
* admin page and the guard cannot disagree about whether an integration is on.
|
|
97
|
+
* That disagreement is the entire failure mode this file's header warns about
|
|
98
|
+
* (Root Cause #42), and it had already happened once, inside the file.
|
|
99
|
+
*/
|
|
100
|
+
export declare function readIntegrationStates(settings: Partial<SiteSettingsDocument> | null | undefined): Record<IntegrationId, IntegrationState>;
|
|
101
|
+
/**
|
|
102
|
+
* Turn a desired state into the settings patch that expresses it.
|
|
103
|
+
*
|
|
104
|
+
* Returned as a nested partial rather than applied here, so the caller does
|
|
105
|
+
* one write and the audit log records one action.
|
|
106
|
+
*/
|
|
107
|
+
export declare function integrationPatch(id: IntegrationId, enabled: boolean): Record<string, unknown>;
|
|
108
|
+
/** Every patch for a bulk enable/disable, merged into one object. */
|
|
109
|
+
export declare function allIntegrationsPatch(enabled: boolean): Record<string, unknown>;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integration switches, and what turning each one OFF actually does.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 This is a VIEW over existing fields, never a second store
|
|
5
|
+
*
|
|
6
|
+
* Three of the four already have an authoritative field:
|
|
7
|
+
*
|
|
8
|
+
* payments -> `payment.razorpayEnabled`
|
|
9
|
+
* email -> `messaging.emailEnabled` (read by `guardSend`)
|
|
10
|
+
* shipping -> `shipping.provider` ("manual" | "shiprocket")
|
|
11
|
+
*
|
|
12
|
+
* Adding an `integrationSwitches.payments` beside those would be a
|
|
13
|
+
* denormalised mirror, and it would drift the first time a path wrote one of
|
|
14
|
+
* the pair and not the other (Root Cause #42) — at which point the admin page
|
|
15
|
+
* says "on" while the code that enforces it reads "off". Only `support.enabled`
|
|
16
|
+
* is new, because only support had nothing.
|
|
17
|
+
*
|
|
18
|
+
* ## 🛑 Off does not mean the same thing twice
|
|
19
|
+
*
|
|
20
|
+
* "Payments off" stops money. "Support off" hides a contact route. "Shipping
|
|
21
|
+
* off" is not even expressible — a parcel still has to move, so the shipping
|
|
22
|
+
* switch selects manual (self-ship) rather than stopping anything.
|
|
23
|
+
*
|
|
24
|
+
* Presenting four interchangeable toggles would invite an admin to flip the
|
|
25
|
+
* expensive one by muscle memory, so every row carries its own
|
|
26
|
+
* `offConsequence` and the dangerous ones are marked.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* 🛑 A `Record<IntegrationId, …>`, so a fifth integration cannot be added
|
|
30
|
+
* without writing its off-consequence. A row with no stated consequence is
|
|
31
|
+
* exactly the interchangeable-toggle failure this file exists to prevent.
|
|
32
|
+
*/
|
|
33
|
+
export const INTEGRATIONS = {
|
|
34
|
+
payments: {
|
|
35
|
+
id: "payments",
|
|
36
|
+
label: "Online payments",
|
|
37
|
+
summary: "Card, UPI and netbanking at checkout, through whichever gateway is live.",
|
|
38
|
+
/*
|
|
39
|
+
* 🛑 The second sentence is the one that matters and it was missing until
|
|
40
|
+
* the route guards were traced. `withOnlinePaymentEnabled` fails CLOSED and
|
|
41
|
+
* guards SIX routes including `/api/payment/verify` and the Razorpay
|
|
42
|
+
* WEBHOOK — so flipping this switch while a buyer is on the gateway's page
|
|
43
|
+
* abandons that payment: the money is captured and the callback that would
|
|
44
|
+
* have placed the order 404s. Razorpay retries on 5xx, NOT on 404, so it is
|
|
45
|
+
* not retried either.
|
|
46
|
+
*
|
|
47
|
+
* Ungating those two routes does not fix it — `providers.config.ts` only
|
|
48
|
+
* registers the gateway when this flag is on, so a completion path would
|
|
49
|
+
* reach `ManualPaymentProvider` and fail differently. The real fix is a
|
|
50
|
+
* bigger change (keep the provider registered for completion while refusing
|
|
51
|
+
* new orders), so until then this is a WARNING an admin must read rather
|
|
52
|
+
* than a hazard they discover.
|
|
53
|
+
*/
|
|
54
|
+
offConsequence: "Buyers can no longer pay online. Checkout falls back to the manual methods (cash, UPI with a proof upload), which need an admin to verify every payment by hand. 🛑 Payments already in progress are abandoned — a buyer part-way through the gateway may be charged without an order being placed. Switch off when nobody is checking out.",
|
|
55
|
+
severe: true,
|
|
56
|
+
// Both gateways, because the enforcing guard is an OR across them. See the
|
|
57
|
+
// note on `readIntegrationStates`.
|
|
58
|
+
field: "payment.razorpayEnabled + payment.phonepeEnabled",
|
|
59
|
+
routeGuard: "gated",
|
|
60
|
+
routeGuardNote: "withOnlinePaymentEnabled, on all six /api/payment routes. Predates this page; this page now reads the same expression so the two cannot disagree.",
|
|
61
|
+
failClosed: true,
|
|
62
|
+
},
|
|
63
|
+
email: {
|
|
64
|
+
id: "email",
|
|
65
|
+
label: "Email sending (Resend)",
|
|
66
|
+
summary: "Order, offer and account emails.",
|
|
67
|
+
offConsequence: "Customer email stops. STAFF email is deliberately unaffected — the daily digest is how an operator notices a stuck payment or a new ticket, so switching customer mail off must not blind you.",
|
|
68
|
+
severe: true,
|
|
69
|
+
field: "messaging.emailEnabled",
|
|
70
|
+
routeGuard: "none",
|
|
71
|
+
/*
|
|
72
|
+
* 🛑 Not an oversight, and not a gap to close later. Email has no inbound
|
|
73
|
+
* route to guard — it is an OUTBOUND effect, and `guardSend` already
|
|
74
|
+
* enforces this flag at the single place that sends. Adding a route guard
|
|
75
|
+
* would mean inventing a route to guard.
|
|
76
|
+
*/
|
|
77
|
+
routeGuardNote: "Nothing to gate: email is outbound. guardSend() enforces this flag at sendEmail(), the only sender.",
|
|
78
|
+
failClosed: false,
|
|
79
|
+
},
|
|
80
|
+
shipping: {
|
|
81
|
+
id: "shipping",
|
|
82
|
+
label: "Courier booking (Shiprocket)",
|
|
83
|
+
summary: "We book the courier and pay the freight.",
|
|
84
|
+
/*
|
|
85
|
+
* Not "shipping stops". The switch selects the PROVIDER, and manual is a
|
|
86
|
+
* working path, not a degraded one — it is the self-ship flow sellers
|
|
87
|
+
* already use and claim postage back through.
|
|
88
|
+
*/
|
|
89
|
+
offConsequence: "We stop booking couriers. Sellers ship themselves and claim the postage back — the flow they already have. Nothing stops moving; in-flight shipments are unaffected.",
|
|
90
|
+
severe: false,
|
|
91
|
+
field: "shipping.provider",
|
|
92
|
+
routeGuard: "none",
|
|
93
|
+
/*
|
|
94
|
+
* 🛑 `/api/shipping/quote` must stay OPEN when this is off, and that is the
|
|
95
|
+
* whole reason this field exists rather than a blanket wrapper over every
|
|
96
|
+
* integration's routes.
|
|
97
|
+
*
|
|
98
|
+
* "Off" here means `provider: "manual"`, which is a WORKING path, not a
|
|
99
|
+
* disabled one. 404ing the quote route would break the self-ship flow that
|
|
100
|
+
* switching the integration off is specifically selecting — the guard would
|
|
101
|
+
* take out the fallback it is falling back to.
|
|
102
|
+
*/
|
|
103
|
+
routeGuardNote: "Deliberately ungated: off means provider=manual, a working path. Guarding /api/shipping/quote would break the self-ship flow this switch selects.",
|
|
104
|
+
failClosed: false,
|
|
105
|
+
},
|
|
106
|
+
support: {
|
|
107
|
+
id: "support",
|
|
108
|
+
label: "Support & tickets",
|
|
109
|
+
summary: "The buyer's contact form and ticket history.",
|
|
110
|
+
/*
|
|
111
|
+
* The owner chose the widest reading of "off", and the consequence is real
|
|
112
|
+
* enough that the row has to state it plainly rather than let an admin
|
|
113
|
+
* discover it.
|
|
114
|
+
*/
|
|
115
|
+
offConsequence: "The ticket surface disappears for buyers — no new tickets, no ticket list, no nav entry. The public Help Centre and its FAQs stay up, because self-service is what reduces tickets. Staff keep full access, so open tickets are still workable. A buyer with a conversation in progress loses sight of it.",
|
|
116
|
+
severe: true,
|
|
117
|
+
field: "support.enabled",
|
|
118
|
+
routeGuard: "gated",
|
|
119
|
+
/*
|
|
120
|
+
* 🛑 The line this note draws is RAISING vs ANSWERING, not buyer vs staff,
|
|
121
|
+
* and it decides three surfaces — so it is written down rather than left to
|
|
122
|
+
* be re-derived by whoever next "completes" the gating.
|
|
123
|
+
*
|
|
124
|
+
* GATED (raising): `/api/support/tickets` GET and POST, and a server layout
|
|
125
|
+
* over all of `/user/support/*`. The GET too, because gating only the write
|
|
126
|
+
* leaves a buyer looking at a ticket list they cannot reply to.
|
|
127
|
+
*
|
|
128
|
+
* NOT GATED (answering): `/admin/support-tickets`, and `/store/support` —
|
|
129
|
+
* which is the seller reading tickets raised ABOUT their store, not filing
|
|
130
|
+
* them. It is served by `SELLER_ENDPOINTS.SUPPORT`, a store-scoped endpoint,
|
|
131
|
+
* so it is untouched by the gate above and that is correct: the owner's rule
|
|
132
|
+
* is that open tickets stay workable, and a seller answering one is on the
|
|
133
|
+
* same side of that line as staff.
|
|
134
|
+
*
|
|
135
|
+
* EXEMPT: `unban_request`, the formal appeal channel. It already bypasses
|
|
136
|
+
* the soft-ban guard and the ticket cap, and an integration toggle must not
|
|
137
|
+
* be able to close a channel a soft ban is explicitly forbidden from
|
|
138
|
+
* closing.
|
|
139
|
+
*/
|
|
140
|
+
routeGuardNote: "Gated on the RAISING side only: /api/support/tickets (GET+POST) and a server layout over /user/support/*. /store/support and /admin/support-tickets stay open — answering, not raising. unban_request is exempt.",
|
|
141
|
+
failClosed: false,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
/** Display order. Money first, because it is what an admin came to check. */
|
|
145
|
+
export const INTEGRATION_ORDER = [
|
|
146
|
+
"payments",
|
|
147
|
+
"email",
|
|
148
|
+
"shipping",
|
|
149
|
+
"support",
|
|
150
|
+
];
|
|
151
|
+
/**
|
|
152
|
+
* Read each switch out of a settings document.
|
|
153
|
+
*
|
|
154
|
+
* 🛑 Defaults differ per integration and are NOT a uniform `?? false`:
|
|
155
|
+
* `razorpayEnabled` genuinely defaults off (it is opt-in and the account runs
|
|
156
|
+
* test keys), while email and support default ON because an older settings
|
|
157
|
+
* document with no such field must not read as "we stopped talking to
|
|
158
|
+
* customers".
|
|
159
|
+
*
|
|
160
|
+
* 🛑 THIS FUNCTION IS THE EXPRESSION THE ROUTE GUARDS USE. `withOnlinePaymentEnabled`
|
|
161
|
+
* had hand-written the payments half, and the first version of this file
|
|
162
|
+
* reproduced it WRONG — see the payments line. Both now resolve here, so the
|
|
163
|
+
* admin page and the guard cannot disagree about whether an integration is on.
|
|
164
|
+
* That disagreement is the entire failure mode this file's header warns about
|
|
165
|
+
* (Root Cause #42), and it had already happened once, inside the file.
|
|
166
|
+
*/
|
|
167
|
+
export function readIntegrationStates(settings) {
|
|
168
|
+
return {
|
|
169
|
+
/*
|
|
170
|
+
* 🛑 An OR across BOTH gateways, not `razorpayEnabled` alone.
|
|
171
|
+
*
|
|
172
|
+
* This was wrong when written, and wrong in the direction that matters: the
|
|
173
|
+
* route guard is `razorpayEnabled === true || phonepeEnabled === true`, so
|
|
174
|
+
* with PhonePe on and Razorpay off the page would have reported "payments
|
|
175
|
+
* OFF" while all six /api/payment routes were OPEN and taking money. An
|
|
176
|
+
* admin reading that row would believe they had stopped payments.
|
|
177
|
+
*
|
|
178
|
+
* The guard's own docstring explains why it is an OR and must stay one:
|
|
179
|
+
* these routes are provider-AGNOSTIC — they call `getProviders().payment`,
|
|
180
|
+
* whichever single gateway `providers.config.ts` registered. The question
|
|
181
|
+
* is "is an online gateway live at all", asked once.
|
|
182
|
+
*/
|
|
183
|
+
payments: {
|
|
184
|
+
enabled: settings?.payment?.razorpayEnabled === true ||
|
|
185
|
+
settings?.payment?.phonepeEnabled === true,
|
|
186
|
+
},
|
|
187
|
+
email: { enabled: settings?.messaging?.emailEnabled !== false },
|
|
188
|
+
// "Enabled" means we are booking couriers, i.e. the provider is not manual.
|
|
189
|
+
shipping: { enabled: settings?.shipping?.provider === "shiprocket" },
|
|
190
|
+
support: { enabled: settings?.support?.enabled !== false },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Turn a desired state into the settings patch that expresses it.
|
|
195
|
+
*
|
|
196
|
+
* Returned as a nested partial rather than applied here, so the caller does
|
|
197
|
+
* one write and the audit log records one action.
|
|
198
|
+
*/
|
|
199
|
+
export function integrationPatch(id, enabled) {
|
|
200
|
+
switch (id) {
|
|
201
|
+
case "payments":
|
|
202
|
+
/*
|
|
203
|
+
* 🛑 Writes BOTH gateway flags, and the OFF case is why.
|
|
204
|
+
*
|
|
205
|
+
* The guard is an OR, so clearing only `razorpayEnabled` while PhonePe
|
|
206
|
+
* was on would leave every /api/payment route open — the switch would
|
|
207
|
+
* report success and change nothing. "Disable payments" has to mean no
|
|
208
|
+
* online gateway is live, which is one flag per gateway.
|
|
209
|
+
*
|
|
210
|
+
* The ON case re-enables RAZORPAY only, deliberately asymmetric:
|
|
211
|
+
* `providers.config.ts` prefers Razorpay and ignores PhonePe when both
|
|
212
|
+
* are true (it logs exactly that), so turning both on would set a flag
|
|
213
|
+
* with no effect and make the page claim two gateways where there is
|
|
214
|
+
* one. Choosing a gateway is a different decision from switching
|
|
215
|
+
* payments on, and it belongs in Site Settings → Payment.
|
|
216
|
+
*/
|
|
217
|
+
return enabled
|
|
218
|
+
? { payment: { razorpayEnabled: true } }
|
|
219
|
+
: { payment: { razorpayEnabled: false, phonepeEnabled: false } };
|
|
220
|
+
case "email":
|
|
221
|
+
return { messaging: { emailEnabled: enabled } };
|
|
222
|
+
case "shipping":
|
|
223
|
+
/*
|
|
224
|
+
* 🛑 `manual`, never a missing value. The field's own docstring is
|
|
225
|
+
* explicit that a provider which fails to resolve must not silently
|
|
226
|
+
* become manual — so the OFF state has to be manual EXPLICITLY, and the
|
|
227
|
+
* absence of a value must never be how we express it.
|
|
228
|
+
*/
|
|
229
|
+
return { shipping: { provider: enabled ? "shiprocket" : "manual" } };
|
|
230
|
+
case "support":
|
|
231
|
+
return { support: { enabled } };
|
|
232
|
+
default: {
|
|
233
|
+
/*
|
|
234
|
+
* Exhaustive. A fifth integration will not compile until someone decides
|
|
235
|
+
* which field it writes — which is the one decision that cannot be
|
|
236
|
+
* guessed later from the switch alone.
|
|
237
|
+
*/
|
|
238
|
+
const unreachable = id;
|
|
239
|
+
return unreachable;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Every patch for a bulk enable/disable, merged into one object. */
|
|
244
|
+
export function allIntegrationsPatch(enabled) {
|
|
245
|
+
const merged = {};
|
|
246
|
+
for (const id of INTEGRATION_ORDER) {
|
|
247
|
+
const patch = integrationPatch(id, enabled);
|
|
248
|
+
for (const [group, value] of Object.entries(patch)) {
|
|
249
|
+
merged[group] = { ...merged[group], ...value };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return merged;
|
|
253
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispute KINDS and the gate each one must pass.
|
|
3
|
+
*
|
|
4
|
+
* ## Why the kind is closed and the reason list is not
|
|
5
|
+
*
|
|
6
|
+
* The owner's two rules are policy:
|
|
7
|
+
*
|
|
8
|
+
* > An item dispute can only be raised after the order was received. A
|
|
9
|
+
* > "not reached" dispute is allowed once the ETD is more than 3 days past.
|
|
10
|
+
*
|
|
11
|
+
* A gate cannot branch on a user-created string. `return-reasons.ts` already
|
|
12
|
+
* makes this argument in its own header — *"'No returns, unless the buyer
|
|
13
|
+
* didn't get what they paid for' is not a policy that can be enforced against
|
|
14
|
+
* a free-text box: it needs a machine-readable statement of what went
|
|
15
|
+
* wrong"* — and it applies with more force here, because these two gates are
|
|
16
|
+
* MUTUALLY EXCLUSIVE.
|
|
17
|
+
*
|
|
18
|
+
* So there are two levels:
|
|
19
|
+
*
|
|
20
|
+
* 1. **`DisputeKind` — closed, here, two values.** It picks the gate.
|
|
21
|
+
* 2. **`disputeReasons` — a staff-creatable Firestore collection.** Every row
|
|
22
|
+
* carries a `kind`, so a new reason inherits an existing gate and cannot
|
|
23
|
+
* invent one. That list is for triage and analytics, never for policy.
|
|
24
|
+
*
|
|
25
|
+
* ## What this replaces
|
|
26
|
+
*
|
|
27
|
+
* `raiseOrderDisputeAction` took `reason: string` and checked only that it was
|
|
28
|
+
* non-empty. So today a buyer can file "it never arrived" on an order that was
|
|
29
|
+
* delivered last week, and "it arrived damaged" on one still in transit — both
|
|
30
|
+
* nonsense, both accepted, and both raising a `severity: "error"` fraud-category
|
|
31
|
+
* admin alert. The gate is the point of this file.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DISPUTE_KIND: readonly ["item", "not_reached"];
|
|
34
|
+
export type DisputeKind = (typeof DISPUTE_KIND)[number];
|
|
35
|
+
/**
|
|
36
|
+
* 🛑 `Record<DisputeKind, string>`, so a third kind is a COMPILE error here
|
|
37
|
+
* rather than a dispute that renders `undefined` and passes no gate.
|
|
38
|
+
*/
|
|
39
|
+
export declare const DISPUTE_KIND_LABEL: Record<DisputeKind, string>;
|
|
40
|
+
export declare function isDisputeKind(value: unknown): value is DisputeKind;
|
|
41
|
+
/**
|
|
42
|
+
* How long after dispatch a parcel is ASSUMED to have been deliverable, when
|
|
43
|
+
* the courier gave us no estimate.
|
|
44
|
+
*
|
|
45
|
+
* 🛑 This exists so the `not_reached` gate is reachable AT ALL. There is no
|
|
46
|
+
* courier ETD on an order today: nothing platform-books (`createShipment` has
|
|
47
|
+
* zero callers), and a self-shipping seller types a tracking number with no
|
|
48
|
+
* estimate attached. Gate solely on a stored ETD and no buyer could ever file
|
|
49
|
+
* a non-delivery dispute — a gate nobody can pass is the same as no feature.
|
|
50
|
+
*
|
|
51
|
+
* Published through `PublishedValueKey` so the policy page and this gate
|
|
52
|
+
* cannot disagree (Phase 9).
|
|
53
|
+
*/
|
|
54
|
+
export declare const DISPUTE_TRANSIT_WINDOW_DAYS = 7;
|
|
55
|
+
/**
|
|
56
|
+
* The owner's "more than 3 days" grace, measured from the ETD.
|
|
57
|
+
*
|
|
58
|
+
* Separate from the transit window on purpose: one is "when should it have
|
|
59
|
+
* arrived", the other is "how long after that before we believe it is lost".
|
|
60
|
+
* Folding them into a single 10 would make neither number explicable on a
|
|
61
|
+
* policy page.
|
|
62
|
+
*/
|
|
63
|
+
export declare const DISPUTE_NOT_REACHED_GRACE_DAYS = 3;
|
|
64
|
+
/** Fields the gates read. Narrow, so a caller need not hold a whole order. */
|
|
65
|
+
export interface DisputeGateOrder {
|
|
66
|
+
deliveryDate?: Date | null;
|
|
67
|
+
completedAt?: Date | null;
|
|
68
|
+
shippingDate?: Date | null;
|
|
69
|
+
estimatedDeliveryDate?: Date | null;
|
|
70
|
+
}
|
|
71
|
+
export type DisputeGateResult = {
|
|
72
|
+
allowed: true;
|
|
73
|
+
}
|
|
74
|
+
/** `message` is buyer-facing and must name the next action, not just refuse. */
|
|
75
|
+
| {
|
|
76
|
+
allowed: false;
|
|
77
|
+
message: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The effective ETD, and WHY we think so.
|
|
81
|
+
*
|
|
82
|
+
* Exported because the buyer-facing form needs to show the date they can file
|
|
83
|
+
* from — telling someone "not yet" without saying when is a dead end.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveDisputeEtd(order: DisputeGateOrder, transitWindowDays?: number): {
|
|
86
|
+
etd: Date;
|
|
87
|
+
source: "courier" | "assumed";
|
|
88
|
+
} | null;
|
|
89
|
+
/** The date from which a `not_reached` dispute becomes filable. */
|
|
90
|
+
export declare function disputeNotReachedFilableFrom(order: DisputeGateOrder, graceDays?: number): Date | null;
|
|
91
|
+
/**
|
|
92
|
+
* 🛑 `Record<DisputeKind, …>`. A third kind cannot be added without deciding
|
|
93
|
+
* what gates it — which is the whole reason the kind is closed.
|
|
94
|
+
*/
|
|
95
|
+
export declare const DISPUTE_GATE: Record<DisputeKind, (order: DisputeGateOrder, now?: Date) => DisputeGateResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Convenience wrapper so callers do not index the record by hand.
|
|
98
|
+
*
|
|
99
|
+
* 🛑 Takes the NARROW `DisputeGateOrder`, and an `OrderDocument` satisfies it
|
|
100
|
+
* structurally — so no adapter is needed and none should be added.
|
|
101
|
+
*
|
|
102
|
+
* A first draft had a `toDisputeGateOrder(order: OrderDocument)` helper, which
|
|
103
|
+
* meant this module imported the order schema while the order schema imports
|
|
104
|
+
* `DisputeKind` from here. That cycle is type-only and therefore erased, so it
|
|
105
|
+
* would have compiled and run — but it is also entirely avoidable, and a
|
|
106
|
+
* module that defines a policy should not depend on the document the policy is
|
|
107
|
+
* applied to. `return-reasons.ts` beside it has no imports at all for the same
|
|
108
|
+
* reason.
|
|
109
|
+
*
|
|
110
|
+
* Nothing leaks by passing the whole order in: what crosses back out is a
|
|
111
|
+
* `DisputeGateResult`, never the document (Root Cause #70).
|
|
112
|
+
*/
|
|
113
|
+
export declare function checkDisputeGate(kind: DisputeKind, order: DisputeGateOrder, now?: Date): DisputeGateResult;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispute KINDS and the gate each one must pass.
|
|
3
|
+
*
|
|
4
|
+
* ## Why the kind is closed and the reason list is not
|
|
5
|
+
*
|
|
6
|
+
* The owner's two rules are policy:
|
|
7
|
+
*
|
|
8
|
+
* > An item dispute can only be raised after the order was received. A
|
|
9
|
+
* > "not reached" dispute is allowed once the ETD is more than 3 days past.
|
|
10
|
+
*
|
|
11
|
+
* A gate cannot branch on a user-created string. `return-reasons.ts` already
|
|
12
|
+
* makes this argument in its own header — *"'No returns, unless the buyer
|
|
13
|
+
* didn't get what they paid for' is not a policy that can be enforced against
|
|
14
|
+
* a free-text box: it needs a machine-readable statement of what went
|
|
15
|
+
* wrong"* — and it applies with more force here, because these two gates are
|
|
16
|
+
* MUTUALLY EXCLUSIVE.
|
|
17
|
+
*
|
|
18
|
+
* So there are two levels:
|
|
19
|
+
*
|
|
20
|
+
* 1. **`DisputeKind` — closed, here, two values.** It picks the gate.
|
|
21
|
+
* 2. **`disputeReasons` — a staff-creatable Firestore collection.** Every row
|
|
22
|
+
* carries a `kind`, so a new reason inherits an existing gate and cannot
|
|
23
|
+
* invent one. That list is for triage and analytics, never for policy.
|
|
24
|
+
*
|
|
25
|
+
* ## What this replaces
|
|
26
|
+
*
|
|
27
|
+
* `raiseOrderDisputeAction` took `reason: string` and checked only that it was
|
|
28
|
+
* non-empty. So today a buyer can file "it never arrived" on an order that was
|
|
29
|
+
* delivered last week, and "it arrived damaged" on one still in transit — both
|
|
30
|
+
* nonsense, both accepted, and both raising a `severity: "error"` fraud-category
|
|
31
|
+
* admin alert. The gate is the point of this file.
|
|
32
|
+
*/
|
|
33
|
+
export const DISPUTE_KIND = ["item", "not_reached"];
|
|
34
|
+
/**
|
|
35
|
+
* 🛑 `Record<DisputeKind, string>`, so a third kind is a COMPILE error here
|
|
36
|
+
* rather than a dispute that renders `undefined` and passes no gate.
|
|
37
|
+
*/
|
|
38
|
+
export const DISPUTE_KIND_LABEL = {
|
|
39
|
+
item: "There's a problem with what I received",
|
|
40
|
+
not_reached: "My order hasn't arrived",
|
|
41
|
+
};
|
|
42
|
+
export function isDisputeKind(value) {
|
|
43
|
+
return typeof value === "string" && DISPUTE_KIND.includes(value);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* How long after dispatch a parcel is ASSUMED to have been deliverable, when
|
|
47
|
+
* the courier gave us no estimate.
|
|
48
|
+
*
|
|
49
|
+
* 🛑 This exists so the `not_reached` gate is reachable AT ALL. There is no
|
|
50
|
+
* courier ETD on an order today: nothing platform-books (`createShipment` has
|
|
51
|
+
* zero callers), and a self-shipping seller types a tracking number with no
|
|
52
|
+
* estimate attached. Gate solely on a stored ETD and no buyer could ever file
|
|
53
|
+
* a non-delivery dispute — a gate nobody can pass is the same as no feature.
|
|
54
|
+
*
|
|
55
|
+
* Published through `PublishedValueKey` so the policy page and this gate
|
|
56
|
+
* cannot disagree (Phase 9).
|
|
57
|
+
*/
|
|
58
|
+
export const DISPUTE_TRANSIT_WINDOW_DAYS = 7;
|
|
59
|
+
/**
|
|
60
|
+
* The owner's "more than 3 days" grace, measured from the ETD.
|
|
61
|
+
*
|
|
62
|
+
* Separate from the transit window on purpose: one is "when should it have
|
|
63
|
+
* arrived", the other is "how long after that before we believe it is lost".
|
|
64
|
+
* Folding them into a single 10 would make neither number explicable on a
|
|
65
|
+
* policy page.
|
|
66
|
+
*/
|
|
67
|
+
export const DISPUTE_NOT_REACHED_GRACE_DAYS = 3;
|
|
68
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
69
|
+
function asDate(v) {
|
|
70
|
+
if (!v)
|
|
71
|
+
return null;
|
|
72
|
+
const d = v instanceof Date ? v : new Date(v);
|
|
73
|
+
return Number.isFinite(d.getTime()) ? d : null;
|
|
74
|
+
}
|
|
75
|
+
function fmt(d) {
|
|
76
|
+
return d.toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The effective ETD, and WHY we think so.
|
|
80
|
+
*
|
|
81
|
+
* Exported because the buyer-facing form needs to show the date they can file
|
|
82
|
+
* from — telling someone "not yet" without saying when is a dead end.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveDisputeEtd(order, transitWindowDays = DISPUTE_TRANSIT_WINDOW_DAYS) {
|
|
85
|
+
const courier = asDate(order.estimatedDeliveryDate);
|
|
86
|
+
if (courier)
|
|
87
|
+
return { etd: courier, source: "courier" };
|
|
88
|
+
const shipped = asDate(order.shippingDate);
|
|
89
|
+
if (shipped) {
|
|
90
|
+
return { etd: new Date(shipped.getTime() + transitWindowDays * DAY_MS), source: "assumed" };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/** The date from which a `not_reached` dispute becomes filable. */
|
|
95
|
+
export function disputeNotReachedFilableFrom(order, graceDays = DISPUTE_NOT_REACHED_GRACE_DAYS) {
|
|
96
|
+
const resolved = resolveDisputeEtd(order);
|
|
97
|
+
if (!resolved)
|
|
98
|
+
return null;
|
|
99
|
+
return new Date(resolved.etd.getTime() + graceDays * DAY_MS);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 🛑 `Record<DisputeKind, …>`. A third kind cannot be added without deciding
|
|
103
|
+
* what gates it — which is the whole reason the kind is closed.
|
|
104
|
+
*/
|
|
105
|
+
export const DISPUTE_GATE = {
|
|
106
|
+
/**
|
|
107
|
+
* You cannot judge an item you have not seen.
|
|
108
|
+
*
|
|
109
|
+
* `completedAt` counts as well as `deliveryDate`: a buyer who pressed "Mark
|
|
110
|
+
* complete" has told us they received it, and refusing them because no
|
|
111
|
+
* courier scan landed would be refusing the more reliable signal of the two.
|
|
112
|
+
*/
|
|
113
|
+
item: (order) => {
|
|
114
|
+
if (asDate(order.deliveryDate) || asDate(order.completedAt))
|
|
115
|
+
return { allowed: true };
|
|
116
|
+
return {
|
|
117
|
+
allowed: false,
|
|
118
|
+
message: "You can report a problem with an item once it arrives. If it hasn't arrived, raise a delivery issue instead.",
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
/**
|
|
122
|
+
* Only valid BEFORE receipt, and only once the parcel is genuinely late.
|
|
123
|
+
*
|
|
124
|
+
* Three refusals, each pointing somewhere:
|
|
125
|
+
* - delivered → the wrong kind; offer the item route or a return.
|
|
126
|
+
* - never dispatched → not a courier problem at all. That is the
|
|
127
|
+
* `seller-dispatch` obligation's job, and it is already running.
|
|
128
|
+
* - too early → say the date, not just "not yet".
|
|
129
|
+
*/
|
|
130
|
+
not_reached: (order, now = new Date()) => {
|
|
131
|
+
const delivered = asDate(order.deliveryDate);
|
|
132
|
+
if (delivered) {
|
|
133
|
+
return {
|
|
134
|
+
allowed: false,
|
|
135
|
+
message: `This order was marked delivered on ${fmt(delivered)}. If something is wrong with what arrived, report a problem with the item instead.`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (!asDate(order.shippingDate) && !asDate(order.estimatedDeliveryDate)) {
|
|
139
|
+
return {
|
|
140
|
+
allowed: false,
|
|
141
|
+
message: "This order hasn't been dispatched yet, so there's nothing in transit to chase. We already track late dispatch and will act on it without you needing to do anything.",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const filableFrom = disputeNotReachedFilableFrom(order);
|
|
145
|
+
if (!filableFrom) {
|
|
146
|
+
// Unreachable given the check above; kept rather than asserted, because
|
|
147
|
+
// a thrown non-null assertion here would be a 500 on a buyer action.
|
|
148
|
+
return {
|
|
149
|
+
allowed: false,
|
|
150
|
+
message: "We can't work out an expected delivery date for this order yet. Contact support.",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (now.getTime() < filableFrom.getTime()) {
|
|
154
|
+
return {
|
|
155
|
+
allowed: false,
|
|
156
|
+
message: `It's still within the expected delivery window. If it hasn't arrived by ${fmt(filableFrom)}, you can raise a delivery issue then.`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return { allowed: true };
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Convenience wrapper so callers do not index the record by hand.
|
|
164
|
+
*
|
|
165
|
+
* 🛑 Takes the NARROW `DisputeGateOrder`, and an `OrderDocument` satisfies it
|
|
166
|
+
* structurally — so no adapter is needed and none should be added.
|
|
167
|
+
*
|
|
168
|
+
* A first draft had a `toDisputeGateOrder(order: OrderDocument)` helper, which
|
|
169
|
+
* meant this module imported the order schema while the order schema imports
|
|
170
|
+
* `DisputeKind` from here. That cycle is type-only and therefore erased, so it
|
|
171
|
+
* would have compiled and run — but it is also entirely avoidable, and a
|
|
172
|
+
* module that defines a policy should not depend on the document the policy is
|
|
173
|
+
* applied to. `return-reasons.ts` beside it has no imports at all for the same
|
|
174
|
+
* reason.
|
|
175
|
+
*
|
|
176
|
+
* Nothing leaks by passing the whole order in: what crosses back out is a
|
|
177
|
+
* `DisputeGateResult`, never the document (Root Cause #70).
|
|
178
|
+
*/
|
|
179
|
+
export function checkDisputeGate(kind, order, now) {
|
|
180
|
+
return DISPUTE_GATE[kind](order, now);
|
|
181
|
+
}
|
|
@@ -35,7 +35,7 @@ export declare const REFUND_COPY: {
|
|
|
35
35
|
readonly reasonSelectPlaceholder: "Choose a reason";
|
|
36
36
|
readonly reasonNoteLabel: "Anything else we should know? (optional)";
|
|
37
37
|
readonly alreadyRefundedMessage: "A refund has already been processed on this order. No further disputes or refund requests can be filed.";
|
|
38
|
-
readonly acknowledgments: readonly ["I understand this refund request, once approved, permanently removes my ability to file any dispute or RMA claim on this order.", "I confirm the item has not been used, worn, or damaged by me, and I am returning it in original condition (if a return is required).", "I understand the refund amount may take
|
|
38
|
+
readonly acknowledgments: readonly ["I understand this refund request, once approved, permanently removes my ability to file any dispute or RMA claim on this order.", "I confirm the item has not been used, worn, or damaged by me, and I am returning it in original condition (if a return is required).", "I understand the refund amount may take {{refundBusinessDaysMin}}–{{refundBusinessDaysMax}} business days to reflect and that platform fees may be non-recoverable."];
|
|
39
39
|
};
|
|
40
40
|
readonly siblingPayments: {
|
|
41
41
|
readonly heading: (count: number) => string;
|