@mohasinac/appkit 4.62.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/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/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 +28 -0
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +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/client.d.ts +11 -0
- package/dist/client.js +30 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +5 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +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/OwnerQuickActions.js +24 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +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/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/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/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/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/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 +19 -1
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +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 +54 -6
- 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/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/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/SellerOrdersView.js +129 -27
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- 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/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/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -1
- package/dist/next/routing/route-map.d.ts +20 -0
- package/dist/next/routing/route-map.js +10 -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 +47 -18
- package/dist/schemas/sieve.d.ts +2 -2
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +10 -1
- package/dist/server.js +39 -1
- 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/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/tabs-overflow.js +64 -11
- 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
|
@@ -113,8 +113,29 @@ export function MediaImage({ src, alt, size = "card", priority = false, loading,
|
|
|
113
113
|
* the placeholder, the `<picture>` and the plain `<Image>` cannot disagree
|
|
114
114
|
* about their dimensions — a fallback that occupies a different box from the
|
|
115
115
|
* image it replaces makes a grid jump as things load.
|
|
116
|
+
*
|
|
117
|
+
* 🛑 `max-h-full` IS THE OTHER HALF OF SIZE_BOX, added 2026-10-23. SIZE_BOX
|
|
118
|
+
* alone fixed the auto-height parent and broke the ALREADY-SIZED one: a
|
|
119
|
+
* parent that is 320x128 got a 320x320 box, so `overflow-hidden` cropped it
|
|
120
|
+
* to the top 40% of a letterboxed square. Measured 89 of 143 call sites in
|
|
121
|
+
* that shape — every image preview on the review, document and site-settings
|
|
122
|
+
* surfaces. `max-height:100%` resolves to `none` against an auto-height
|
|
123
|
+
* parent, so it clamps ONLY in the broken case.
|
|
124
|
+
*
|
|
125
|
+
* Measured in a browser (box for each parent shape), not argued:
|
|
126
|
+
*
|
|
127
|
+
* parent w-full aspect +max-h-full +h-full
|
|
128
|
+
* 320x128 definite 320x320 ✗ 320x128 ✓ 320x128 ✓
|
|
129
|
+
* h128, width shrink 320x320 ✗ 320x128 ✓ 320x128 ✓
|
|
130
|
+
* width 200, h AUTO 200x200 ✓ 200x200 ✓ 200x784 ✗
|
|
131
|
+
* 64px thumb stretched
|
|
132
|
+
* by a flex row 64x64 ✓ 64x64 ✓ 64x90 ✗
|
|
133
|
+
*
|
|
134
|
+
* So `h-full` is NOT the fix — it wins over `aspect-ratio` whenever the
|
|
135
|
+
* parent's height is definite, including the height a flex/grid row hands a
|
|
136
|
+
* thumbnail by stretching it. That is why this is a MAX and not a height.
|
|
116
137
|
*/
|
|
117
|
-
const boxClass = fill ? "h-full" : SIZE_BOX[size]
|
|
138
|
+
const boxClass = fill ? "h-full" : `${SIZE_BOX[size]} max-h-full`;
|
|
118
139
|
// Reset error/load/retry state whenever the resolved src actually changes.
|
|
119
140
|
// Without this, a component instance that hit a load failure once (e.g.
|
|
120
141
|
// lost the post-upload propagation race above) latches hasError forever,
|
|
@@ -222,7 +222,7 @@ accept = IMAGE_ACCEPT_ATTR, maxSizeMB = MAX_IMAGE_BYTES / MEGABYTE, label = "Upl
|
|
|
222
222
|
fileInputRef.current.value = "";
|
|
223
223
|
onChange?.("");
|
|
224
224
|
};
|
|
225
|
-
return (_jsxs(Stack, { gap: "3", children: [label && (_jsx(Label, { className: "block", color: "muted", size: "sm", weight: "medium", children: label })), _jsx(Div, { className: "relative", children: preview ? (_jsxs(Stack, { gap: "sm", children: [_jsxs(Div, { className: `relative h-32 max-w-xs mx-auto ${__O.hidden}`, rounded: "lg", border: "default", children: [_jsx(MediaImage, { src: preview, alt: "Preview", size: "card" }), uploading && progress > 0 && (_jsx(Div, { className: "absolute inset-x-0 bottom-0", children: _jsx(Progress, { value: progress, size: "sm" }) }))] }), !uploading && (_jsxs(Row, { wrap: true, gap: "sm", children: [(enableCrop || enableAdvancedCrop) && (_jsx(Button, { type: "button", onClick: () => openEditor(preview), variant: "secondary", size: "sm", children: tMediaEditor("editImage") })), _jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "outline", size: "sm", children: tUpload("change") }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] })) : (_jsxs(_Fragment, { children: [showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: "photo", facingMode: "environment", onCapture: handleCameraCapture, onError: setError })), !showCamera && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "sm", wrap: true, children: [showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), disabled: uploading, variant: "outline", size: "sm", children: uploading ? tUpload("uploading") : tUpload("clickToUpload") })), captureSource !== "file-only" && isCameraSupported && (_jsx(Button, { type: "button", onClick: () => setCaptureMode("camera"), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") })), captureSource !== "file-only" && !isCameraSupported && (_jsx(Button, { type: "button", onClick: () => mobileCaptureRef.current?.click(), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") }))] }), _jsxs(Text, { size: "xs", variant: "secondary", children: [accept === "image/*"
|
|
225
|
+
return (_jsxs(Stack, { gap: "3", children: [label && (_jsx(Label, { className: "block", color: "muted", size: "sm", weight: "medium", children: label })), _jsx(Div, { className: "relative", children: preview ? (_jsxs(Stack, { gap: "sm", children: [_jsxs(Div, { className: `relative w-full h-32 max-w-xs mx-auto ${__O.hidden}`, rounded: "lg", border: "default", children: [_jsx(MediaImage, { src: preview, alt: "Preview", size: "card" }), uploading && progress > 0 && (_jsx(Div, { className: "absolute inset-x-0 bottom-0", children: _jsx(Progress, { value: progress, size: "sm" }) }))] }), !uploading && (_jsxs(Row, { wrap: true, gap: "sm", children: [(enableCrop || enableAdvancedCrop) && (_jsx(Button, { type: "button", onClick: () => openEditor(preview), variant: "secondary", size: "sm", children: tMediaEditor("editImage") })), _jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "outline", size: "sm", children: tUpload("change") }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] })) : (_jsxs(_Fragment, { children: [showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: "photo", facingMode: "environment", onCapture: handleCameraCapture, onError: setError })), !showCamera && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "sm", wrap: true, children: [showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), disabled: uploading, variant: "outline", size: "sm", children: uploading ? tUpload("uploading") : tUpload("clickToUpload") })), captureSource !== "file-only" && isCameraSupported && (_jsx(Button, { type: "button", onClick: () => setCaptureMode("camera"), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") })), captureSource !== "file-only" && !isCameraSupported && (_jsx(Button, { type: "button", onClick: () => mobileCaptureRef.current?.click(), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") }))] }), _jsxs(Text, { size: "xs", variant: "secondary", children: [accept === "image/*"
|
|
226
226
|
? `JPG PNG GIF WebP`
|
|
227
227
|
: accept.split(",").map((a) => a.split("/")[1]?.toUpperCase() ?? a).join(" "), " ", "\u2014 max ", maxSizeMB, "MB"] })] }))] })) }), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: IMAGE_ACCEPT_ATTR, capture: "environment", onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-capture-input" })), helperText && !error && (_jsx(Text, { variant: "secondary", size: "xs", children: helperText })), error && _jsx(Alert, { variant: "error", children: error }), uploading && (_jsxs(Row, { gap: "sm", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploadingProgress", { progress }) })] })), cropPreviewUrl && !enableAdvancedCrop && (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: cropPreviewUrl, onClose: handleCropClose, onSave: handleCropSave })), cropPreviewUrl && enableAdvancedCrop && (_jsx(ImageEditor, { isOpen: showAdvancedEditor, imageUrl: cropPreviewUrl, onClose: handleAdvancedEditorClose, onSave: handleAdvancedEditorSave, aspectRatio: cropAspectRatio }))] }));
|
|
228
228
|
}
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { ORDER_RETURN_WINDOW_DAYS, ORDER_AUTO_CONFIRM_DAYS, } from "../../../_internal/shared/features/orders/config.js";
|
|
38
38
|
import { PROTECTION_CLAIM_WINDOW_HOURS } from "../../../_internal/shared/features/shipping/protections.js";
|
|
39
39
|
import { PAYMENT_WINDOW_MINUTES } from "../../orders/constants/payment-window.js";
|
|
40
|
+
import { hasOpenCourierBlock } from "../../orders/utils/courier-block.js";
|
|
40
41
|
const isPaid = (o) => o.paymentStatus === "paid";
|
|
41
42
|
const notTerminal = (o) => o.status !== "cancelled" && o.status !== "refunded";
|
|
42
43
|
/**
|
|
@@ -87,7 +88,29 @@ export const OBLIGATION_POLICIES = {
|
|
|
87
88
|
anchor: (o) => o.acceptedAt,
|
|
88
89
|
duration: { value: 2, unit: "days", calendar: "business" },
|
|
89
90
|
warnAt: [{ value: 8, unit: "hours", calendar: "calendar" }],
|
|
90
|
-
|
|
91
|
+
/*
|
|
92
|
+
* 🛑 `!hasOpenCourierBlock(o)` — DO NOT REMOVE, and do not "simplify" it
|
|
93
|
+
* into the stored field.
|
|
94
|
+
*
|
|
95
|
+
* This is `onDue: "act"`, so firing cancels the order and refunds the
|
|
96
|
+
* buyer with the seller recorded as having failed to dispatch. When the
|
|
97
|
+
* platform's own courier wallet is empty, nothing the seller can do makes
|
|
98
|
+
* the booking succeed — so without this guard our funding state cancels a
|
|
99
|
+
* paid order and blames them for it.
|
|
100
|
+
*
|
|
101
|
+
* The engine re-evaluates `precondition` at fire time and closes
|
|
102
|
+
* `superseded` when it returns false, so the clock PAUSES while blocked and
|
|
103
|
+
* resumes when the block clears. No new mechanism, no stored timer.
|
|
104
|
+
*
|
|
105
|
+
* 🛑 This is the ONLY `onDue: "act"` policy that needs the guard, and the
|
|
106
|
+
* others were checked rather than assumed: `hasDispatched` is
|
|
107
|
+
* `Boolean(o.shippingDate)`, so `delivery-confirm` (which REQUIRES
|
|
108
|
+
* `hasDispatched`) and everything post-delivery can never fire on an order
|
|
109
|
+
* that was never booked. `seller-accept` requires `!o.acceptedAt`, and a
|
|
110
|
+
* booking is only attempted after acceptance. Adding the guard there too
|
|
111
|
+
* would be redundant and would imply nobody traced it.
|
|
112
|
+
*/
|
|
113
|
+
precondition: (o) => isPaid(o) && notTerminal(o) && !hasDispatched(o) && !hasOpenCourierBlock(o),
|
|
91
114
|
blockedBy: ["seller-accept"],
|
|
92
115
|
onDue: "act",
|
|
93
116
|
publishedCopyKey: "obligations.sellerDispatch",
|
|
@@ -32,7 +32,16 @@ function legacyFallback(order) {
|
|
|
32
32
|
method,
|
|
33
33
|
transactionId: order.paymentTransactionId ?? order.paymentId,
|
|
34
34
|
proofUrl: order.paymentProofUrl,
|
|
35
|
-
|
|
35
|
+
/*
|
|
36
|
+
* 🛑 D46/D47 — `amountDue`, falling back to `totalPrice`. This figure is
|
|
37
|
+
* what a MANUAL payer is told to transfer and what an admin verifies the
|
|
38
|
+
* proof against, so on a points-funded order `totalPrice` would ask the
|
|
39
|
+
* buyer for money the order does not owe — and then the admin would reject
|
|
40
|
+
* a correct ₹750 UTR for not matching ₹1,000.
|
|
41
|
+
*
|
|
42
|
+
* `?? totalPrice`, never `?? 0`: absent means no points were used.
|
|
43
|
+
*/
|
|
44
|
+
amount: order.amountDue ?? order.totalPrice,
|
|
36
45
|
verificationMethod: method === "cod" ? "cod_collection" : method === "manual" ? "manual_review" : "order_status",
|
|
37
46
|
};
|
|
38
47
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OrderSourceContext } from "../schemas/firestore";
|
|
2
|
+
export interface OrderProvenanceCardProps {
|
|
3
|
+
sourceContext?: OrderSourceContext;
|
|
4
|
+
/** Falls back to the configured default currency when omitted. */
|
|
5
|
+
currency?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function OrderProvenanceCard({ sourceContext, currency, className, }: OrderProvenanceCardProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* OrderProvenanceCard — renders `OrderDocument.sourceContext`, the record of
|
|
5
|
+
* HOW the buyer earned the right to buy at this price.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this component had to be written
|
|
8
|
+
*
|
|
9
|
+
* `sourceContext` is a six-variant discriminated union, assembled at a real
|
|
10
|
+
* cost — an auction pays one `findByProductSorted`, an offer one `findById` —
|
|
11
|
+
* written on every order by both checkout paths, mapped through to the client
|
|
12
|
+
* type by `orderDocumentToOrder` (adapters.ts:359) … and, until this file,
|
|
13
|
+
* rendered in **zero** `.tsx` files anywhere in the repo.
|
|
14
|
+
*
|
|
15
|
+
* That is Root Cause #52/#57: the data was already present and complete, and
|
|
16
|
+
* no surface was ever wired to present it. The buyer could not see what they
|
|
17
|
+
* had agreed, the seller could not see what they had accepted, and support
|
|
18
|
+
* could not see either.
|
|
19
|
+
*
|
|
20
|
+
* ## What it deliberately does NOT do
|
|
21
|
+
*
|
|
22
|
+
* **`standard` renders nothing.** Its only field is the list price, which the
|
|
23
|
+
* order total already shows — a card reading "Standard purchase" on every
|
|
24
|
+
* ordinary order is noise that would teach people to ignore the card on the
|
|
25
|
+
* orders where it matters. It is still an explicit `case`, so the switch stays
|
|
26
|
+
* exhaustive.
|
|
27
|
+
*
|
|
28
|
+
* **A saving is shown only where it is real.** Not every variant records a
|
|
29
|
+
* displaced price: `auction-buy-now` has `listPrice`, `offer-accepted` has
|
|
30
|
+
* `listPriceAtOffer`, and `auction-won` has only `startingBid` — which is a
|
|
31
|
+
* floor, not an asking price, so presenting the gap as a discount would be a
|
|
32
|
+
* fabrication. Where a displaced price exists it is shown only when it is
|
|
33
|
+
* strictly greater than what was paid.
|
|
34
|
+
*
|
|
35
|
+
* **Nothing is fetched.** It is handed the context the page already loaded; a
|
|
36
|
+
* component that fetched would fire one request per opened drawer per portal.
|
|
37
|
+
*/
|
|
38
|
+
import { Badge, Card, DescriptionField, Heading, Row, Stack, Text } from "../../../ui/index.js";
|
|
39
|
+
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
40
|
+
import { formatDate } from "../../../utils/date.formatter.js";
|
|
41
|
+
/**
|
|
42
|
+
* A date on a `sourceContext` survives a Firestore round trip as a Timestamp,
|
|
43
|
+
* an ISO string or a Date depending on which path loaded it, so normalise
|
|
44
|
+
* rather than assuming. An unparseable value renders an em-dash — never
|
|
45
|
+
* `new Date()`, which would invent a timestamp for a record that has none
|
|
46
|
+
* (the rule `OrderStatusTimeline` already follows).
|
|
47
|
+
*/
|
|
48
|
+
function dateText(value) {
|
|
49
|
+
if (value === null || value === undefined)
|
|
50
|
+
return "—";
|
|
51
|
+
const raw = typeof value === "object" && value !== null && "toDate" in value
|
|
52
|
+
? value.toDate()
|
|
53
|
+
: value;
|
|
54
|
+
const d = raw instanceof Date ? raw : new Date(raw);
|
|
55
|
+
return Number.isFinite(d.getTime()) ? formatDate(d) : "—";
|
|
56
|
+
}
|
|
57
|
+
function plural(n, one, many) {
|
|
58
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
59
|
+
}
|
|
60
|
+
function contentFor(ctx, currency) {
|
|
61
|
+
const money = (n) => formatCurrency(n, currency);
|
|
62
|
+
switch (ctx.path) {
|
|
63
|
+
case "standard":
|
|
64
|
+
// Deliberately silent — see the header. Explicit so the switch is exhaustive.
|
|
65
|
+
return null;
|
|
66
|
+
case "auction-won": {
|
|
67
|
+
const rows = [
|
|
68
|
+
{ label: "Winning bid", value: money(ctx.winningBidAmount) },
|
|
69
|
+
{ label: "Bids in the auction", value: String(ctx.bidCount) },
|
|
70
|
+
{ label: "Starting bid", value: money(ctx.startingBid) },
|
|
71
|
+
];
|
|
72
|
+
if (ctx.runnerUpAmount !== undefined) {
|
|
73
|
+
rows.push({ label: "Runner-up bid", value: money(ctx.runnerUpAmount) });
|
|
74
|
+
}
|
|
75
|
+
if (ctx.reservePrice !== undefined) {
|
|
76
|
+
rows.push({
|
|
77
|
+
label: "Reserve",
|
|
78
|
+
value: `${money(ctx.reservePrice)} — ${ctx.reserveMet ? "met" : "not met"}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
rows.push({ label: "Auction ended", value: dateText(ctx.auctionEndedAt) });
|
|
82
|
+
return {
|
|
83
|
+
title: "Won at auction",
|
|
84
|
+
tone: "success",
|
|
85
|
+
summary: ctx.bidCount > 1
|
|
86
|
+
? `Highest of ${plural(ctx.bidCount, "bid", "bids")}.`
|
|
87
|
+
: "The only bid placed.",
|
|
88
|
+
rows,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
case "auction-buy-now": {
|
|
92
|
+
const rows = [
|
|
93
|
+
{ label: "Buy Now price", value: money(ctx.buyNowPrice) },
|
|
94
|
+
{ label: "List price", value: money(ctx.listPrice) },
|
|
95
|
+
{
|
|
96
|
+
label: "Standing bid beaten",
|
|
97
|
+
value: ctx.standingBidAtBuyout > 0 ? money(ctx.standingBidAtBuyout) : "none",
|
|
98
|
+
},
|
|
99
|
+
{ label: "Bids at buyout", value: String(ctx.bidCount) },
|
|
100
|
+
{ label: "Bought", value: dateText(ctx.boughtAt) },
|
|
101
|
+
];
|
|
102
|
+
return {
|
|
103
|
+
title: "Bought now, ending the auction",
|
|
104
|
+
tone: "info",
|
|
105
|
+
/*
|
|
106
|
+
* A buyout IS a real bid (the schema's own note: recording bidCount 0
|
|
107
|
+
* would bake a falsehood into an immutable record), so the summary
|
|
108
|
+
* must not claim nobody had bid when somebody had.
|
|
109
|
+
*/
|
|
110
|
+
summary: ctx.bidCount > 0
|
|
111
|
+
? `Taken at the Buy Now price while ${plural(ctx.bidCount, "bid was", "bids were")} live.`
|
|
112
|
+
: "Taken at the Buy Now price before any bids were placed.",
|
|
113
|
+
rows,
|
|
114
|
+
saved: ctx.listPrice > ctx.buyNowPrice ? ctx.listPrice - ctx.buyNowPrice : undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
case "offer-accepted": {
|
|
118
|
+
const rows = [
|
|
119
|
+
{ label: "Agreed price", value: money(ctx.offeredAmount) },
|
|
120
|
+
{ label: "Asking price when offered", value: money(ctx.listPriceAtOffer) },
|
|
121
|
+
];
|
|
122
|
+
if (ctx.counterAmount !== undefined) {
|
|
123
|
+
rows.push({ label: "Final counter", value: money(ctx.counterAmount) });
|
|
124
|
+
}
|
|
125
|
+
rows.push({ label: "Negotiation", value: plural(ctx.counterRounds, "round", "rounds") }, { label: "Accepted", value: dateText(ctx.acceptedAt) });
|
|
126
|
+
return {
|
|
127
|
+
title: "Price agreed by offer",
|
|
128
|
+
tone: "primary",
|
|
129
|
+
summary: ctx.counterRounds > 1
|
|
130
|
+
? `Agreed after ${plural(ctx.counterRounds, "round", "rounds")} of counter-offers.`
|
|
131
|
+
: "Accepted outright, with no counter-offer.",
|
|
132
|
+
rows,
|
|
133
|
+
saved: ctx.listPriceAtOffer > ctx.offeredAmount
|
|
134
|
+
? ctx.listPriceAtOffer - ctx.offeredAmount
|
|
135
|
+
: undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
case "pre-order":
|
|
139
|
+
return {
|
|
140
|
+
title: "Pre-order",
|
|
141
|
+
tone: "warning",
|
|
142
|
+
summary: ctx.balanceDue > 0
|
|
143
|
+
? "A deposit was taken at checkout; the balance is due before dispatch."
|
|
144
|
+
: "Paid in full at checkout.",
|
|
145
|
+
rows: [
|
|
146
|
+
{ label: "Deposit paid", value: money(ctx.depositAmount) },
|
|
147
|
+
{ label: "Balance due", value: money(ctx.balanceDue) },
|
|
148
|
+
{ label: "Expected delivery", value: dateText(ctx.expectedDeliveryDate) },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
case "prize-draw":
|
|
152
|
+
return {
|
|
153
|
+
title: "Prize draw entry",
|
|
154
|
+
tone: "info",
|
|
155
|
+
summary: ctx.revealMode === "instant"
|
|
156
|
+
? "The result was revealed immediately."
|
|
157
|
+
: "The result is revealed at the scheduled draw.",
|
|
158
|
+
rows: [
|
|
159
|
+
{ label: "Entries bought", value: plural(ctx.entryCount, "entry", "entries") },
|
|
160
|
+
{ label: "Price per entry", value: money(ctx.pricePerEntry) },
|
|
161
|
+
{ label: "Reveal", value: ctx.revealMode === "instant" ? "Instant" : "Scheduled" },
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
default: {
|
|
165
|
+
/*
|
|
166
|
+
* Exhaustive. A seventh variant — the shared-purchase path S5a still
|
|
167
|
+
* needs — will not compile rather than silently rendering nothing, which
|
|
168
|
+
* is the failure mode of `default: return null` and is precisely how this
|
|
169
|
+
* union came to have six populated variants and no reader at all.
|
|
170
|
+
*/
|
|
171
|
+
const unreachable = ctx;
|
|
172
|
+
return unreachable;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export function OrderProvenanceCard({ sourceContext, currency, className, }) {
|
|
177
|
+
// Every order written before W2 has no context at all — absence is normal
|
|
178
|
+
// and is not an error state to announce.
|
|
179
|
+
if (!sourceContext)
|
|
180
|
+
return null;
|
|
181
|
+
const content = contentFor(sourceContext, currency);
|
|
182
|
+
if (!content)
|
|
183
|
+
return null;
|
|
184
|
+
return (_jsx(Card, { variant: "outlined", padding: "md", className: className, children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 3, size: "sm", children: content.title }), content.saved !== undefined ? (_jsx(Badge, { variant: "success", children: `Saved ${formatCurrency(content.saved, currency)}` })) : null] }), content.summary ? (_jsx(Text, { size: "sm", color: "muted", children: content.summary })) : null, _jsx(Row, { as: "dl", wrap: true, gap: "md", children: content.rows.map((row) => (_jsx(DescriptionField, { label: row.label, value: row.value }, row.label))) })] }) }));
|
|
185
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DisputeGateOrder, type DisputeKind } from "../../../_internal/shared/features/orders/dispute-reasons";
|
|
2
|
+
export interface RaiseDisputeFormProps {
|
|
3
|
+
/** The gate reads four dates off this; an `OrderDocument` satisfies it. */
|
|
4
|
+
order: DisputeGateOrder;
|
|
5
|
+
/** `GET ?kind=` / `POST` — supplied by the consumer so appkit holds no route. */
|
|
6
|
+
reasonsEndpoint: string;
|
|
7
|
+
onSubmit: (input: {
|
|
8
|
+
kind: DisputeKind;
|
|
9
|
+
reasonId: string;
|
|
10
|
+
detail?: string;
|
|
11
|
+
}) => Promise<void>;
|
|
12
|
+
onDone?: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function RaiseDisputeForm({ order, reasonsEndpoint, onSubmit, onDone, }: RaiseDisputeFormProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The buyer's dispute form: pick a kind, pick a reason, say what happened.
|
|
5
|
+
*
|
|
6
|
+
* ## Why the kind comes first
|
|
7
|
+
*
|
|
8
|
+
* The two kinds have MUTUALLY EXCLUSIVE gates — an item problem needs the
|
|
9
|
+
* order to have arrived, a non-delivery needs it not to have. So the kind is
|
|
10
|
+
* the first choice, it scopes the reason list, and it decides which gate the
|
|
11
|
+
* server will apply. Offering one merged list of reasons would mean a buyer
|
|
12
|
+
* could pick "it never arrived" on a delivered order and have the server
|
|
13
|
+
* refuse them, which reads as the form being broken rather than as the wrong
|
|
14
|
+
* choice.
|
|
15
|
+
*
|
|
16
|
+
* ## The gate is checked HERE for the affordance and AGAIN on the server
|
|
17
|
+
*
|
|
18
|
+
* 🛑 This copy decides what to OFFER. The server decides what to ACCEPT, and
|
|
19
|
+
* it is the authority — `raiseOrderDisputeAction` re-runs `checkDisputeGate`
|
|
20
|
+
* on the real order. Showing a kind the server would refuse is the dead-end
|
|
21
|
+
* shape (CLAUDE.md's rule for `OrderCompletionPanel`: never offer a button the
|
|
22
|
+
* server will refuse), and trusting this copy alone would be a client-side
|
|
23
|
+
* security gate.
|
|
24
|
+
*/
|
|
25
|
+
import { useCallback, useMemo, useState } from "react";
|
|
26
|
+
import { DISPUTE_KIND, DISPUTE_KIND_LABEL, checkDisputeGate, disputeNotReachedFilableFrom, } from "../../../_internal/shared/features/orders/dispute-reasons.js";
|
|
27
|
+
import { Alert, Button, Div, FieldTextarea, Form, Stack, Text, useToast } from "../../../ui/index.js";
|
|
28
|
+
import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
|
|
29
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
30
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
31
|
+
export function RaiseDisputeForm({ order, reasonsEndpoint, onSubmit, onDone, }) {
|
|
32
|
+
const { showToast } = useToast();
|
|
33
|
+
const [kind, setKind] = useState(null);
|
|
34
|
+
const [reasonId, setReasonId] = useState(null);
|
|
35
|
+
const [detail, setDetail] = useState("");
|
|
36
|
+
const [isPending, setIsPending] = useState(false);
|
|
37
|
+
/*
|
|
38
|
+
* Which kinds this order can actually support, with the server's own gate.
|
|
39
|
+
* A kind that would be refused is shown DISABLED with the reason rather than
|
|
40
|
+
* hidden: "why can't I report this?" is a question the form should answer,
|
|
41
|
+
* and a silently missing option looks like a bug.
|
|
42
|
+
*/
|
|
43
|
+
const kindStates = useMemo(() => DISPUTE_KIND.map((k) => ({
|
|
44
|
+
kind: k,
|
|
45
|
+
label: DISPUTE_KIND_LABEL[k],
|
|
46
|
+
gate: checkDisputeGate(k, order),
|
|
47
|
+
})), [order]);
|
|
48
|
+
const filableFrom = useMemo(() => disputeNotReachedFilableFrom(order), [order]);
|
|
49
|
+
/*
|
|
50
|
+
* 🛑 Keyed on `kind` so switching kind refetches. `PaginatedSelect` caches
|
|
51
|
+
* per `loadOptions` identity, so a stable callback would keep showing the
|
|
52
|
+
* previous kind's reasons — which is the one thing that must never happen
|
|
53
|
+
* here, because a mismatched reason is refused by the server.
|
|
54
|
+
*/
|
|
55
|
+
const loadReasons = useCallback(async (query, page) => {
|
|
56
|
+
if (!kind)
|
|
57
|
+
return { items: [], hasMore: false };
|
|
58
|
+
const res = await fetch(`${reasonsEndpoint}?kind=${encodeURIComponent(kind)}`);
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
/*
|
|
61
|
+
* Surfaced, not swallowed into an empty list. An empty picker is
|
|
62
|
+
* indistinguishable from "there are no reasons", and the buyer would
|
|
63
|
+
* sit on a required field with nothing in it (Root Cause #59).
|
|
64
|
+
*/
|
|
65
|
+
throw new Error("Could not load the list of reasons. Please try again.");
|
|
66
|
+
}
|
|
67
|
+
const json = (await res.json());
|
|
68
|
+
const rows = json.data?.items ?? [];
|
|
69
|
+
const q = query.trim().toLowerCase();
|
|
70
|
+
const filtered = q ? rows.filter((r) => r.label.toLowerCase().includes(q)) : rows;
|
|
71
|
+
return {
|
|
72
|
+
// `items`, not `options` — checked against `AsyncPage` rather than
|
|
73
|
+
// guessed; the wrong key yields an empty picker with no error.
|
|
74
|
+
items: filtered.map((r) => ({ value: r.id, label: r.label })),
|
|
75
|
+
// The route caps at 50 and the list is short by design; there is no
|
|
76
|
+
// second page to promise. `page` is unused for the same reason.
|
|
77
|
+
hasMore: false,
|
|
78
|
+
};
|
|
79
|
+
}, [kind, reasonsEndpoint]);
|
|
80
|
+
const chooseKind = (next) => {
|
|
81
|
+
setKind(next);
|
|
82
|
+
// Clearing is required: a reason from the previous kind would be refused.
|
|
83
|
+
setReasonId(null);
|
|
84
|
+
};
|
|
85
|
+
const submit = async () => {
|
|
86
|
+
if (!kind || !reasonId)
|
|
87
|
+
return;
|
|
88
|
+
setIsPending(true);
|
|
89
|
+
try {
|
|
90
|
+
await onSubmit({ kind, reasonId, detail: detail.trim() || undefined });
|
|
91
|
+
showToast("Thanks — we've recorded it and will look into it.", "success");
|
|
92
|
+
onDone?.();
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// The server's message names the next action; show it verbatim rather
|
|
96
|
+
// than replacing it with a generic failure.
|
|
97
|
+
showToast(normalizeError(err).message, "error");
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
setIsPending(false);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return (_jsx(Form, { onSubmit: (e) => e.preventDefault(), children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "What's the problem?" }), kindStates.map((k) => (_jsxs(Div, { children: [_jsx(Button, { variant: kind === k.kind ? "primary" : "secondary", disabled: !k.gate.allowed, onClick: () => chooseKind(k.kind), children: k.label }), !k.gate.allowed ? (_jsx(Text, { size: "xs", color: "muted", children: k.gate.message })) : null] }, k.kind)))] }), kind ? (_jsxs(_Fragment, { children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Reason" }), _jsx(PaginatedSelect, { ariaLabel: "Reason for the dispute",
|
|
104
|
+
/*
|
|
105
|
+
* 🛑 `?? null`, not `?? ""`. PaginatedSelect renders its
|
|
106
|
+
* placeholder only on `value == null`; an empty string paints a
|
|
107
|
+
* BLANK trigger with no prompt, which reads as a broken control
|
|
108
|
+
* rather than an unmade choice.
|
|
109
|
+
*/
|
|
110
|
+
value: reasonId ?? null, onChange: (v) => setReasonId(v), loadOptions: loadReasons, placeholder: "Pick the closest match\u2026" }), _jsx(FieldTextarea, { name: "detail", label: "What happened? (optional)", value: detail, onChange: setDetail, maxLength: 2000 }), kind === "not_reached" && filableFrom ? (_jsx(Alert, { variant: "info", children: `We'll chase the courier. If it turns up before we finish, let us know.` })) : null, _jsx(Button, { type: "submit", action: ACTIONS.SUPPORT["raise-dispute"], isLoading: isPending, disabled: !reasonId, onClick: submit, children: "Report this" })] })) : null] }) }));
|
|
111
|
+
}
|
|
@@ -28,11 +28,19 @@ import { FormShellContext, useFormShellState, FormErrorSummary, FieldTextarea, }
|
|
|
28
28
|
import { SectionForm, useSectionFormNav } from "../../shell/index.js";
|
|
29
29
|
import { z } from "zod";
|
|
30
30
|
import { REFUND_COPY } from "../../../_internal/shared/features/orders/refund-copy.js";
|
|
31
|
+
import { resolvePublishedCopy, resolvePublishedValues, } from "../../../_internal/shared/content/published-values.js";
|
|
31
32
|
import { RETURN_REASON_LABEL, selectableReturnReasons, } from "../../../_internal/shared/features/orders/return-reasons.js";
|
|
32
33
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
33
34
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
34
35
|
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
35
36
|
const ACK_COUNT = REFUND_COPY.request.acknowledgments.length;
|
|
37
|
+
/*
|
|
38
|
+
* Phase 9 — code-derived values, resolved once. No settings argument: the only
|
|
39
|
+
* token in this copy is the refund timeline, which has no setting (see the 🛑
|
|
40
|
+
* on `refundBusinessDaysMin` in the registry), so passing settings would imply
|
|
41
|
+
* an editable number that does not exist.
|
|
42
|
+
*/
|
|
43
|
+
const PUBLISHED_VALUES = resolvePublishedValues();
|
|
36
44
|
export function RefundRequestView({ order, onSubmitRequest, isLoading = false, className = "", }) {
|
|
37
45
|
/*
|
|
38
46
|
* Does anything in this order carry the final-sale term? Read from the
|
|
@@ -83,7 +91,17 @@ export function RefundRequestView({ order, onSubmitRequest, isLoading = false, c
|
|
|
83
91
|
fields: ["acknowledged"],
|
|
84
92
|
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "sm", children: [REFUND_COPY.request.acknowledgments.map((text, i) => (_jsx(Checkbox, { checked: values.acknowledged[i] ?? false, onChange: () => onChange({
|
|
85
93
|
acknowledged: values.acknowledged.map((v, idx) => idx === i ? !v : v),
|
|
86
|
-
}),
|
|
94
|
+
}),
|
|
95
|
+
/*
|
|
96
|
+
* 🛑 Phase 9 — resolved, because this is an ACKNOWLEDGEMENT
|
|
97
|
+
* the buyer ticks. `REFUND_COPY`'s refund-timeline line now
|
|
98
|
+
* carries `{{refundBusinessDaysMin}}`/`Max` so it has one
|
|
99
|
+
* owner instead of being the third of three contradicting
|
|
100
|
+
* statements — and an unresolved token inside something a
|
|
101
|
+
* buyer is agreeing to is the strongest reason the unknown-key
|
|
102
|
+
* rule leaves `{{…}}` VISIBLE rather than blanking it.
|
|
103
|
+
*/
|
|
104
|
+
label: _jsx(Text, { size: "sm", className: "leading-snug", children: resolvePublishedCopy(text, PUBLISHED_VALUES) }), "aria-label": `Acknowledgment ${i + 1}`, "aria-invalid": errors.acknowledged ? "true" : undefined }, i))), errors.acknowledged && (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: errors.acknowledged }))] })),
|
|
87
105
|
},
|
|
88
106
|
], [order.total, order.currency, hasFinalSaleLine, allowedReasons]);
|
|
89
107
|
const nav = useSectionFormNav(sections, draft);
|
|
@@ -6,6 +6,10 @@ export { OrderSiblingPayments } from "./OrderSiblingPayments";
|
|
|
6
6
|
export type { OrderSiblingPaymentsProps } from "./OrderSiblingPayments";
|
|
7
7
|
export { OrderPaymentSummary } from "./OrderPaymentSummary";
|
|
8
8
|
export type { OrderPaymentSummaryProps } from "./OrderPaymentSummary";
|
|
9
|
+
export { OrderProvenanceCard } from "./OrderProvenanceCard";
|
|
10
|
+
export type { OrderProvenanceCardProps } from "./OrderProvenanceCard";
|
|
11
|
+
export { RaiseDisputeForm } from "./RaiseDisputeForm";
|
|
12
|
+
export type { RaiseDisputeFormProps } from "./RaiseDisputeForm";
|
|
9
13
|
export { OrderStatusTimeline } from "./OrderStatusTimeline";
|
|
10
14
|
export type { OrderStatusTimelineProps } from "./OrderStatusTimeline";
|
|
11
15
|
export { RefundHistoryTable } from "./RefundHistoryTable";
|
|
@@ -3,6 +3,8 @@ export { OrderAddonBadges, } from "./OrderAddonBadges.js";
|
|
|
3
3
|
export { OrderFilters, ORDER_FILTER_KEYS, ORDER_ADMIN_SORT_OPTIONS, ORDER_SELLER_SORT_OPTIONS, ORDER_USER_SORT_OPTIONS, getOrderFilterKeys, getOrderSortOptions, } from "./OrderFilters.js";
|
|
4
4
|
export { OrderSiblingPayments } from "./OrderSiblingPayments.js";
|
|
5
5
|
export { OrderPaymentSummary } from "./OrderPaymentSummary.js";
|
|
6
|
+
export { OrderProvenanceCard } from "./OrderProvenanceCard.js";
|
|
7
|
+
export { RaiseDisputeForm } from "./RaiseDisputeForm.js";
|
|
6
8
|
export { OrderStatusTimeline } from "./OrderStatusTimeline.js";
|
|
7
9
|
export { RefundHistoryTable } from "./RefundHistoryTable.js";
|
|
8
10
|
export { RefundRequestView } from "./RefundRequestView.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Order PROVENANCE tabs — the one definition, shared by the buyer's and the
|
|
3
|
+
* seller's order lists.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this file exists
|
|
6
|
+
*
|
|
7
|
+
* The buyer's list had these tabs and the seller's did not. A seller could see
|
|
8
|
+
* every order and could not ask "show me the ones I accepted an offer on" or
|
|
9
|
+
* "show me the auction wins", which is exactly the split that changes how they
|
|
10
|
+
* are handled: an auction win has a forfeit deadline, an accepted offer has an
|
|
11
|
+
* agreed price that is not the listing price.
|
|
12
|
+
*
|
|
13
|
+
* Copying the array into the second view would have been the ninth
|
|
14
|
+
* hand-written enumeration of one union in this codebase (Root Cause #61), so
|
|
15
|
+
* the labels are a `Record<OrderType, string>` instead: a sixth lane is a
|
|
16
|
+
* COMPILE error here rather than a tab that silently never appears.
|
|
17
|
+
*
|
|
18
|
+
* 🛑 Three things about these ids, each of which has already cost a bug:
|
|
19
|
+
*
|
|
20
|
+
* 1. **Every id is EXACTLY a stored `OrderDocument.orderType` value.** Firestore
|
|
21
|
+
* `==` is byte-exact, so a tab id that is merely plausible returns zero rows
|
|
22
|
+
* forever with no error anywhere (Root Cause #33).
|
|
23
|
+
*
|
|
24
|
+
* 2. **`preorder` has NO HYPHEN and that is not a typo to tidy up.** `OrderType`
|
|
25
|
+
* spells it `preorder`; `ListingType` spells the same concept `pre-order`.
|
|
26
|
+
* Two unions, two spellings, and the tab must match the one actually stored.
|
|
27
|
+
*
|
|
28
|
+
* 3. **`standard` must be filtered IN MEMORY, never as a Firestore `==`.**
|
|
29
|
+
* Orders written before `orderType` existed carry no such field, and an
|
|
30
|
+
* equality excludes every document that lacks it — so the "Normal" tab would
|
|
31
|
+
* hide every legacy order while looking like it worked. `mergeOrderScopeFilter`
|
|
32
|
+
* and the server-side list both already honour this; see `ORDER_LANE_ALL` for
|
|
33
|
+
* the "no filter" sentinel.
|
|
34
|
+
*/
|
|
35
|
+
import type { OrderType } from "../utils/order-splitter";
|
|
36
|
+
/**
|
|
37
|
+
* The "All" tab's id. An empty string rather than a word, so it can never
|
|
38
|
+
* collide with a real `orderType` value and so `state.orderType ? … : null`
|
|
39
|
+
* reads as "no filter" at every call site.
|
|
40
|
+
*/
|
|
41
|
+
export declare const ORDER_LANE_ALL: "";
|
|
42
|
+
/**
|
|
43
|
+
* 🛑 A `Record<OrderType, string>`, deliberately. Omitting a lane does not
|
|
44
|
+
* compile, which is the whole point — the buyer's list shipped with four tabs
|
|
45
|
+
* for five lanes, so pre-order and prize-draw orders were reachable only by
|
|
46
|
+
* landing on "All" and scrolling.
|
|
47
|
+
*/
|
|
48
|
+
export declare const ORDER_LANE_LABELS: Record<OrderType, string>;
|
|
49
|
+
/**
|
|
50
|
+
* The stored `orderType` values a lane filter may name, for route-side
|
|
51
|
+
* validation.
|
|
52
|
+
*
|
|
53
|
+
* Derived from `ORDER_LANE_LABELS`, so it cannot drift from the tabs. Both
|
|
54
|
+
* `/api/user/orders` and `/api/store/orders` had — or would have needed — their
|
|
55
|
+
* own hand-written copy of these five strings; this is the eleventh
|
|
56
|
+
* enumeration of one union in this codebase collapsed into the union itself
|
|
57
|
+
* (Root Cause #61).
|
|
58
|
+
*
|
|
59
|
+
* 🛑 Validating against it is load-bearing, not hygiene: an unrecognised value
|
|
60
|
+
* interpolated into a Sieve filter string is a caller-controlled field name
|
|
61
|
+
* reaching the query builder.
|
|
62
|
+
*/
|
|
63
|
+
export declare const VALID_ORDER_TYPES: ReadonlySet<string>;
|
|
64
|
+
export interface OrderLaneTab {
|
|
65
|
+
id: string;
|
|
66
|
+
label: string;
|
|
67
|
+
}
|
|
68
|
+
/** `All` first, then every lane in `LANE_ORDER`. */
|
|
69
|
+
export declare const ORDER_LANE_TABS: readonly OrderLaneTab[];
|