@mohasinac/appkit 4.33.1 → 4.34.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/_internal/server/features/checkout/actions.js +9 -1
- package/dist/_internal/server/features/checkout/expand-order-items.js +9 -0
- package/dist/_internal/server/features/media/contextGuards.d.ts +3 -3
- package/dist/_internal/server/features/media/contextGuards.js +19 -5
- package/dist/_internal/server/features/orders/actions.d.ts +9 -0
- package/dist/_internal/server/features/orders/actions.js +47 -4
- package/dist/_internal/server/features/orders/adapters.js +10 -0
- package/dist/_internal/server/features/refunds/actions.d.ts +9 -1
- package/dist/_internal/server/features/refunds/actions.js +36 -3
- package/dist/_internal/server/features/seo/sitemap.js +2 -2
- package/dist/_internal/server/features/site-settings/adapters.js +1 -1
- package/dist/_internal/server/functions/scheduled.d.ts +1 -2
- package/dist/_internal/server/functions/scheduled.js +8 -9
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +15 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +15 -6
- package/dist/_internal/server/jobs/handlers/index.d.ts +0 -1
- package/dist/_internal/server/jobs/handlers/index.js +0 -1
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +12 -0
- package/dist/_internal/shared/features/orders/refund-copy.js +12 -0
- package/dist/_internal/shared/features/orders/return-reasons.d.ts +59 -0
- package/dist/_internal/shared/features/orders/return-reasons.js +96 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +30 -0
- package/dist/_internal/shared/features/orders/schema.js +20 -0
- package/dist/_internal/shared/features/products/schema.d.ts +12 -0
- package/dist/_internal/shared/features/products/schema.js +4 -1
- package/dist/_internal/shared/media/limits.d.ts +14 -0
- package/dist/_internal/shared/media/limits.js +29 -0
- package/dist/client.d.ts +9 -1
- package/dist/client.js +19 -1
- package/dist/features/admin/components/AdminProductEditorView.js +6 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +2 -6
- package/dist/features/admin/components/index.d.ts +0 -2
- package/dist/features/admin/components/index.js +0 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +1 -1
- package/dist/features/admin/schemas/firestore.d.ts +0 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
- package/dist/features/auth/schemas/firestore.d.ts +1 -0
- package/dist/features/contact/components/ContactForm.js +46 -31
- package/dist/features/media/upload/MediaUploadList.d.ts +16 -1
- package/dist/features/media/upload/MediaUploadList.js +58 -8
- package/dist/features/orders/components/RefundRequestView.d.ts +26 -10
- package/dist/features/orders/components/RefundRequestView.js +108 -24
- package/dist/features/orders/schemas/firestore.d.ts +42 -0
- package/dist/features/orders/types/index.d.ts +24 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +5 -3
- package/dist/features/products/components/ProductDetailPageView.js +14 -2
- package/dist/features/products/components/ProductFeatureBadges.d.ts +9 -1
- package/dist/features/products/components/ProductFeatureBadges.js +17 -1
- package/dist/features/products/components/ProductForm.js +46 -10
- package/dist/features/products/components/ProductGrid.js +10 -1
- package/dist/features/products/constants/final-sale.d.ts +40 -0
- package/dist/features/products/constants/final-sale.js +40 -0
- package/dist/features/products/index.d.ts +1 -0
- package/dist/features/products/index.js +1 -0
- package/dist/features/products/schemas/firestore.d.ts +19 -2
- package/dist/features/products/schemas/firestore.js +4 -0
- package/dist/features/products/types/index.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.d.ts +7 -0
- package/dist/features/seller/components/SellerProductShell.js +82 -11
- package/dist/features/seller/index.d.ts +1 -0
- package/dist/features/seller/index.js +1 -0
- package/dist/features/seller/schemas/product-form.d.ts +304 -0
- package/dist/features/seller/schemas/product-form.js +132 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +117 -0
- package/dist/features/seller/utils/product-draft-mapping.js +147 -0
- package/dist/features/shell/SectionForm.js +62 -7
- package/dist/features/shell/form-sections.js +4 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +16 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +32 -6
- package/dist/features/tester/seed-data/events-tester-seed-data.js +77 -5
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +23 -9
- package/dist/features/tester/seed-data/products-tester-seed-data.js +65 -49
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +182 -0
- package/dist/features/tester/seed-data/tester-window.d.ts +25 -0
- package/dist/features/tester/seed-data/tester-window.js +66 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +12 -3
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/schemas/registry.d.ts +271 -0
- package/dist/schemas/registry.js +9 -0
- package/dist/seed/_helpers/final-sale-fixtures.d.ts +35 -0
- package/dist/seed/_helpers/final-sale-fixtures.js +61 -0
- package/dist/seed/products-art-seed-data.js +9 -1
- package/dist/seed/products-auctions-seed-data.js +2 -1
- package/dist/seed/products-classifieds-seed-data.js +2 -1
- package/dist/seed/products-digital-codes-seed-data.js +2 -1
- package/dist/seed/products-live-items-seed-data.js +2 -1
- package/dist/seed/products-preorders-seed-data.d.ts +2 -138
- package/dist/seed/products-preorders-seed-data.js +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +2 -1
- package/dist/seed/products-standard-seed-data.js +2 -1
- package/dist/seed/products-stickers-seed-data.js +9 -1
- package/dist/seed/users-seed-data.js +107 -4
- package/dist/server.d.ts +6 -2
- package/dist/server.js +15 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/motion.d.ts +12 -0
- package/dist/tokens/motion.js +12 -0
- package/dist/ui/components/CollapsibleSection.d.ts +18 -1
- package/dist/ui/components/CollapsibleSection.js +20 -3
- package/dist/ui/components/Motion.js +17 -1
- package/package.json +1 -1
|
@@ -1068,7 +1068,15 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
1068
1068
|
orderId: primaryOrderId,
|
|
1069
1069
|
type: "partial",
|
|
1070
1070
|
amount: refundAmount,
|
|
1071
|
-
|
|
1071
|
+
/*
|
|
1072
|
+
* A SYSTEM refund, not a buyer claim: items the buyer paid for turned
|
|
1073
|
+
* out to be unavailable, so the seller could not deliver what was
|
|
1074
|
+
* described. `not_received` is the honest code and — not incidentally —
|
|
1075
|
+
* one final sale may never block, which is correct here: the buyer is
|
|
1076
|
+
* owed this money whatever the listing's return terms said.
|
|
1077
|
+
*/
|
|
1078
|
+
reasonCode: "not_received",
|
|
1079
|
+
reasonNote: `Automatic refund — ${unavailablePaid.length} item(s) unavailable at checkout: ${unavailablePaid.map((u) => u.productTitle).join(", ")}`,
|
|
1072
1080
|
method: "razorpay",
|
|
1073
1081
|
razorpayPaymentId,
|
|
1074
1082
|
confirmIrrevocable: true,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* rows back under one header, which it was already written to do.
|
|
19
19
|
*/
|
|
20
20
|
import { lineTotalFor, allocateAcrossMembers } from "../../../shared/checkout/order-math";
|
|
21
|
+
import { isFinalSale } from "../../../../features/products/constants/final-sale";
|
|
21
22
|
/**
|
|
22
23
|
* Expand one cart line into the order rows it should produce.
|
|
23
24
|
*
|
|
@@ -45,6 +46,10 @@ export function expandCartLineToOrderRows(item, representative, productById) {
|
|
|
45
46
|
quantity: item.quantity,
|
|
46
47
|
unitPrice: lineTotal / (item.quantity || 1),
|
|
47
48
|
totalPrice: lineTotal,
|
|
49
|
+
// Snapshot the return terms the buyer is agreeing to right now. Resolved
|
|
50
|
+
// through `isFinalSale` so this is always an explicit boolean, even
|
|
51
|
+
// though the product field is optional and absent means true.
|
|
52
|
+
finalSale: isFinalSale(representative),
|
|
48
53
|
// Legacy collapsed bundle rows (no groupMembers) keep their old shape so
|
|
49
54
|
// orders already in flight and their UI stay untouched.
|
|
50
55
|
...(item.bundleCategorySlug && item.bundleProductIds
|
|
@@ -73,6 +78,10 @@ export function expandCartLineToOrderRows(item, representative, productById) {
|
|
|
73
78
|
quantity,
|
|
74
79
|
unitPrice: quantity > 0 ? totalPrice / quantity : totalPrice,
|
|
75
80
|
totalPrice,
|
|
81
|
+
// Per MEMBER, not per line: a bundle may mix a final-sale item with a
|
|
82
|
+
// returnable one, and the refund gate reads each row separately so the
|
|
83
|
+
// returnable member stays returnable.
|
|
84
|
+
finalSale: isFinalSale(product),
|
|
76
85
|
groupSlug,
|
|
77
86
|
groupTitle: item.groupTitle ?? item.productTitle,
|
|
78
87
|
groupSource,
|
|
@@ -13,12 +13,12 @@ import { type AllowedMime } from "../../../shared/media/limits";
|
|
|
13
13
|
import { type MediaFilenameContext } from "../../../../utils/id-generators";
|
|
14
14
|
import type { FirestoreDocument } from "@mohasinac/appkit";
|
|
15
15
|
export declare const CONTEXT_LIMITS: {
|
|
16
|
-
readonly PRODUCT_IMAGE_MAX:
|
|
16
|
+
readonly PRODUCT_IMAGE_MAX: number;
|
|
17
17
|
readonly PRODUCT_VIDEO_MAX: 1;
|
|
18
18
|
readonly REVIEW_IMAGE_MAX: 5;
|
|
19
19
|
readonly REVIEW_VIDEO_MAX: 1;
|
|
20
|
-
readonly AUCTION_IMAGE_MAX:
|
|
21
|
-
readonly PREORDER_IMAGE_MAX:
|
|
20
|
+
readonly AUCTION_IMAGE_MAX: number;
|
|
21
|
+
readonly PREORDER_IMAGE_MAX: number;
|
|
22
22
|
readonly EVENT_COVER_MAX: 1;
|
|
23
23
|
readonly EVENT_IMAGE_MAX: 10;
|
|
24
24
|
readonly EVENT_WINNER_IMAGE_MAX: 5;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Pure logic — no Firebase SDK calls, no `Buffer` work — so it's safe to
|
|
10
10
|
* import from any server route.
|
|
11
11
|
*/
|
|
12
|
-
import { classifyMime, MIME_TO_EXT, } from "../../../shared/media/limits";
|
|
12
|
+
import { classifyMime, MIME_TO_EXT, PRODUCT_IMAGE_INDEX_MAX, PRODUCT_MAX_VIDEOS, } from "../../../shared/media/limits";
|
|
13
13
|
import { generateMediaFilename, validateMediaFilename, } from "../../../../utils/id-generators";
|
|
14
14
|
/**
|
|
15
15
|
* W1-51 — validate the generated filename before returning ok:true so a
|
|
@@ -29,12 +29,26 @@ function okWithFilename(ctx, filename, ext) {
|
|
|
29
29
|
// Per-context limits — mirrored from the previous inline constants in
|
|
30
30
|
// /api/media/upload. Adjust here, not at the call site.
|
|
31
31
|
export const CONTEXT_LIMITS = {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/*
|
|
33
|
+
* These are INDEX ceilings, not counts — `indexGuard` compares them against
|
|
34
|
+
* the `index` baked into the media filename, and `generateMediaFilename`
|
|
35
|
+
* gives the main image index 1 with the gallery running 2..N. So the index
|
|
36
|
+
* space is one larger than the gallery allowance, which is exactly what
|
|
37
|
+
* `PRODUCT_IMAGE_INDEX_MAX` encodes.
|
|
38
|
+
*
|
|
39
|
+
* Getting that wrong was a live 400: the gallery was capped at 5 and sent
|
|
40
|
+
* its 5th image as index 6, so the last slot of every product form was
|
|
41
|
+
* rejected by the signed-URL endpoint.
|
|
42
|
+
*
|
|
43
|
+
* FLAT across listing types — auctions and pre-orders are products with
|
|
44
|
+
* different sale mechanics, not products that deserve fewer photos.
|
|
45
|
+
*/
|
|
46
|
+
PRODUCT_IMAGE_MAX: PRODUCT_IMAGE_INDEX_MAX,
|
|
47
|
+
PRODUCT_VIDEO_MAX: PRODUCT_MAX_VIDEOS,
|
|
34
48
|
REVIEW_IMAGE_MAX: 5,
|
|
35
49
|
REVIEW_VIDEO_MAX: 1,
|
|
36
|
-
AUCTION_IMAGE_MAX:
|
|
37
|
-
PREORDER_IMAGE_MAX:
|
|
50
|
+
AUCTION_IMAGE_MAX: PRODUCT_IMAGE_INDEX_MAX,
|
|
51
|
+
PREORDER_IMAGE_MAX: PRODUCT_IMAGE_INDEX_MAX,
|
|
38
52
|
EVENT_COVER_MAX: 1,
|
|
39
53
|
EVENT_IMAGE_MAX: 10,
|
|
40
54
|
EVENT_WINNER_IMAGE_MAX: 5,
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { type ActionResult } from "@mohasinac/appkit/server";
|
|
2
2
|
export declare function createOrderAction(input: unknown): Promise<ActionResult<unknown>>;
|
|
3
3
|
export declare function cancelOrderAction(input: unknown): Promise<ActionResult<unknown>>;
|
|
4
|
+
/**
|
|
5
|
+
* Buyer requests a return on a delivered order.
|
|
6
|
+
*
|
|
7
|
+
* Two things this did not do before, both of which made the feature unusable:
|
|
8
|
+
*
|
|
9
|
+
* 1. it parsed `cancelOrderSchema` and then **discarded the reason**, so an
|
|
10
|
+
* order reached `return_requested` with nothing recorded about why;
|
|
11
|
+
* 2. it had no caller anywhere — the whole path was unreachable.
|
|
12
|
+
*/
|
|
4
13
|
export declare function requestReturnAction(input: unknown): Promise<ActionResult<unknown>>;
|
|
5
14
|
export declare function updateOrderStatusAction(input: unknown): Promise<ActionResult<unknown>>;
|
|
6
15
|
export declare function attachPaymentProofAction(orderId: string, proof: {
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { wrapAction } from "@mohasinac/appkit/server";
|
|
3
3
|
import { orderRepository, storeRepository, userRepository } from "../../../../repositories";
|
|
4
4
|
import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
|
|
5
|
-
import { createOrderSchema, updateOrderStatusSchema, cancelOrderSchema, } from "../../../shared/features/orders/schema";
|
|
5
|
+
import { createOrderSchema, updateOrderStatusSchema, cancelOrderSchema, returnRequestSchema, } from "../../../shared/features/orders/schema";
|
|
6
|
+
import { isFinalSaleExempt } from "../../../shared/features/orders/return-reasons";
|
|
7
|
+
import { REFUND_COPY } from "../../../shared/features/orders/refund-copy";
|
|
6
8
|
import { assertOrderCancellable, assertReturnWindowOpen } from "./service";
|
|
7
9
|
import { ValidationError } from "../../../shared/errors/index";
|
|
8
10
|
import { OrderNotFoundError, OrderOwnershipError } from "../../../shared/features/orders/errors";
|
|
@@ -37,14 +39,55 @@ export async function cancelOrderAction(input) {
|
|
|
37
39
|
return orderRepository.cancelOrder(parsed.data.orderId, parsed.data.reason ?? "Cancelled by user");
|
|
38
40
|
});
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Buyer requests a return on a delivered order.
|
|
44
|
+
*
|
|
45
|
+
* Two things this did not do before, both of which made the feature unusable:
|
|
46
|
+
*
|
|
47
|
+
* 1. it parsed `cancelOrderSchema` and then **discarded the reason**, so an
|
|
48
|
+
* order reached `return_requested` with nothing recorded about why;
|
|
49
|
+
* 2. it had no caller anywhere — the whole path was unreachable.
|
|
50
|
+
*/
|
|
40
51
|
export async function requestReturnAction(input) {
|
|
41
52
|
return wrapAction(async () => {
|
|
42
53
|
const user = await requireRoleUser(["buyer", "seller", "admin"]);
|
|
43
|
-
const parsed =
|
|
54
|
+
const parsed = returnRequestSchema.safeParse(input);
|
|
44
55
|
if (!parsed.success)
|
|
45
56
|
throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
const { orderId, reasonCode, reasonNote, itemIds } = parsed.data;
|
|
58
|
+
// Ownership + the 7-day post-delivery window.
|
|
59
|
+
await assertReturnWindowOpen(orderId, user.uid);
|
|
60
|
+
/*
|
|
61
|
+
* The final-sale gate, in the same shape as `processRefundAction`'s.
|
|
62
|
+
* Both entry points need it: this one is where the buyer asks, that one
|
|
63
|
+
* is where the money moves, and a buyer must not be able to lodge a
|
|
64
|
+
* request that is guaranteed to be refused at the second gate.
|
|
65
|
+
*
|
|
66
|
+
* Read from the order's per-line snapshot, never the live product.
|
|
67
|
+
*/
|
|
68
|
+
if (!isFinalSaleExempt(reasonCode)) {
|
|
69
|
+
const order = await orderRepository.findById(orderId);
|
|
70
|
+
if (!order)
|
|
71
|
+
throw new OrderNotFoundError(orderId);
|
|
72
|
+
const scopedItems = itemIds?.length
|
|
73
|
+
? (order.items ?? []).filter((i) => itemIds.includes(i.productId))
|
|
74
|
+
: (order.items ?? []);
|
|
75
|
+
if (scopedItems.some((i) => i.finalSale === true)) {
|
|
76
|
+
throw new ValidationError(REFUND_COPY.request.finalSaleBlockedMessage);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/*
|
|
80
|
+
* Record the reason BEFORE flipping the status. A status change is what
|
|
81
|
+
* every notification and dashboard query keys on, so a seller reading
|
|
82
|
+
* the row the moment it lands must already find the reason on it.
|
|
83
|
+
*/
|
|
84
|
+
await orderRepository.update(orderId, {
|
|
85
|
+
returnReasonCode: reasonCode,
|
|
86
|
+
...(reasonNote ? { returnReasonNote: reasonNote } : {}),
|
|
87
|
+
returnRequestedAt: new Date(),
|
|
88
|
+
...(itemIds?.length ? { returnRequestedItemIds: itemIds } : {}),
|
|
89
|
+
});
|
|
90
|
+
return orderRepository.updateStatus(orderId, "return_requested");
|
|
48
91
|
});
|
|
49
92
|
}
|
|
50
93
|
export async function updateOrderStatusAction(input) {
|
|
@@ -20,6 +20,10 @@ export function orderDocumentToOrder(doc) {
|
|
|
20
20
|
...(item.cancelledQuantity != null
|
|
21
21
|
? { cancelledQuantity: item.cancelledQuantity }
|
|
22
22
|
: {}),
|
|
23
|
+
// Snapshotted return terms. Without this the buyer's return form
|
|
24
|
+
// cannot tell a final-sale line from a returnable one and offers
|
|
25
|
+
// every reason on every order (Root Cause #57).
|
|
26
|
+
...(item.finalSale != null ? { finalSale: item.finalSale } : {}),
|
|
23
27
|
}))
|
|
24
28
|
: [
|
|
25
29
|
{
|
|
@@ -56,6 +60,12 @@ export function orderDocumentToOrder(doc) {
|
|
|
56
60
|
// missing value as "standard" rather than filtering it out.
|
|
57
61
|
orderType: doc.orderType,
|
|
58
62
|
offerId: doc.offerId,
|
|
63
|
+
// Refund-eligibility fields. All three were declared on the document and
|
|
64
|
+
// mapped nowhere, so every buyer-facing refund surface saw `undefined`.
|
|
65
|
+
...(doc.isNonRefundable != null ? { isNonRefundable: doc.isNonRefundable } : {}),
|
|
66
|
+
...(doc.contestable != null ? { contestable: doc.contestable } : {}),
|
|
67
|
+
...(doc.returnReasonCode ? { returnReasonCode: doc.returnReasonCode } : {}),
|
|
68
|
+
...(doc.returnReasonNote ? { returnReasonNote: doc.returnReasonNote } : {}),
|
|
59
69
|
subtotal,
|
|
60
70
|
shippingCost: shippingCost || undefined,
|
|
61
71
|
discount: discount || undefined,
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { type ActionResult } from "@mohasinac/appkit/server";
|
|
2
2
|
import type { RefundType } from "../../../../features/orders/schemas";
|
|
3
|
+
import { type ReturnReason } from "../../../shared/features/orders/return-reasons";
|
|
3
4
|
export type ProcessRefundInput = {
|
|
4
5
|
orderId: string;
|
|
5
6
|
type: RefundType;
|
|
6
7
|
/** Decimal rupees. Must be > 0 and ≤ remaining refundable amount on the order. */
|
|
7
8
|
amount: number;
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Coded reason. Replaced a free-text `reason: string` — final sale has to
|
|
11
|
+
* refuse a change-of-mind return while always allowing a "didn't get what I
|
|
12
|
+
* paid for" claim, and that decision cannot be made against prose.
|
|
13
|
+
*/
|
|
14
|
+
reasonCode: ReturnReason;
|
|
15
|
+
/** Optional human elaboration, stored alongside the code. */
|
|
16
|
+
reasonNote?: string;
|
|
9
17
|
/** Product/item ids affected (for partial refunds). */
|
|
10
18
|
itemIds?: string[];
|
|
11
19
|
/** Required: caller must explicitly confirm this action is irrevocable. */
|
|
@@ -22,6 +22,8 @@ import { orderRepository } from "../../../..";
|
|
|
22
22
|
import { ORDER_FIELDS } from "../../../../constants/field-names";
|
|
23
23
|
import { NotFoundError, ValidationError } from "../../../../errors";
|
|
24
24
|
import { serverLogger } from "../../../../monitoring";
|
|
25
|
+
import { isFinalSaleExempt, RETURN_REASON_LABEL, } from "../../../shared/features/orders/return-reasons";
|
|
26
|
+
import { REFUND_COPY } from "../../../shared/features/orders/refund-copy";
|
|
25
27
|
import { applyRefundDeductionAction } from "../payouts/actions";
|
|
26
28
|
import { getAdminDb } from "../../../../providers/db-firebase";
|
|
27
29
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION, } from "../../../../features/products/schemas/firestore";
|
|
@@ -86,10 +88,32 @@ export async function processRefundAction(input) {
|
|
|
86
88
|
if (input.amount > order.totalPrice) {
|
|
87
89
|
throw new ValidationError("Refund amount exceeds order total");
|
|
88
90
|
}
|
|
89
|
-
// Guard: non-refundable orders (prize draws, bundles).
|
|
91
|
+
// Guard: non-refundable orders (prize draws, bundles). A property of the
|
|
92
|
+
// listing TYPE, decided by the platform — checked first because it
|
|
93
|
+
// admits no exception, not even a seller-fault one.
|
|
90
94
|
if (order.isNonRefundable) {
|
|
91
95
|
throw new ValidationError("This order is marked non-refundable and cannot be refunded.");
|
|
92
96
|
}
|
|
97
|
+
/*
|
|
98
|
+
* Guard: final sale.
|
|
99
|
+
*
|
|
100
|
+
* Unlike `isNonRefundable` this is per-LINE and reason-dependent. A
|
|
101
|
+
* seller-fault claim is never blocked, however the listing was sold —
|
|
102
|
+
* "no returns" is a statement about changing your mind, not a waiver of
|
|
103
|
+
* the seller's obligation to ship what was described.
|
|
104
|
+
*
|
|
105
|
+
* Read from the ORDER's snapshot, never from the product: the buyer
|
|
106
|
+
* agreed to the terms shown at checkout, and re-reading the live product
|
|
107
|
+
* would let a seller flip the switch afterwards.
|
|
108
|
+
*/
|
|
109
|
+
if (!isFinalSaleExempt(input.reasonCode)) {
|
|
110
|
+
const scopedItems = input.itemIds?.length
|
|
111
|
+
? (order.items ?? []).filter((i) => input.itemIds.includes(i.productId))
|
|
112
|
+
: (order.items ?? []);
|
|
113
|
+
if (scopedItems.some((i) => i.finalSale === true)) {
|
|
114
|
+
throw new ValidationError(REFUND_COPY.request.finalSaleBlockedMessage);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
93
117
|
// SB-UNI-N — digital-code refund gate: block if code is already claimed (revealed).
|
|
94
118
|
const digitalCodeItems = (order.items ?? []).filter((i) => (i.listingType ?? "standard") === "digital-code");
|
|
95
119
|
await assertDigitalCodesNotClaimed(input.orderId, digitalCodeItems);
|
|
@@ -109,7 +133,14 @@ export async function processRefundAction(input) {
|
|
|
109
133
|
type: input.type,
|
|
110
134
|
amount: input.amount,
|
|
111
135
|
...(input.itemIds?.length ? { itemIds: input.itemIds } : {}),
|
|
112
|
-
|
|
136
|
+
reasonCode: input.reasonCode,
|
|
137
|
+
// `reason` stays the human-readable string every existing refund row
|
|
138
|
+
// and every rendering of RefundHistoryTable already reads. It is now
|
|
139
|
+
// DERIVED from the code rather than typed freehand, so the two can
|
|
140
|
+
// never disagree about why a refund happened.
|
|
141
|
+
reason: input.reasonNote
|
|
142
|
+
? `${RETURN_REASON_LABEL[input.reasonCode]} — ${input.reasonNote}`
|
|
143
|
+
: RETURN_REASON_LABEL[input.reasonCode],
|
|
113
144
|
refundedAt: now,
|
|
114
145
|
refundedBy: input.refundedBy,
|
|
115
146
|
...(razorpayRefundId ? { razorpayRefundId } : {}),
|
|
@@ -134,7 +165,9 @@ export async function processRefundAction(input) {
|
|
|
134
165
|
orderId: input.orderId,
|
|
135
166
|
refundId,
|
|
136
167
|
refundedAmount: input.amount,
|
|
137
|
-
|
|
168
|
+
// Same derived string the refund event records, so the payout
|
|
169
|
+
// deduction and the refund history read identically.
|
|
170
|
+
reason: event.reason,
|
|
138
171
|
}).catch(() => { });
|
|
139
172
|
}
|
|
140
173
|
return { success: true, refundId };
|
|
@@ -16,8 +16,8 @@ const PRODUCT_SLUG = "slug";
|
|
|
16
16
|
const PRODUCT_UPDATED_AT = "updatedAt";
|
|
17
17
|
/**
|
|
18
18
|
* Tester-sandbox fixtures must never reach the public sitemap. They are created
|
|
19
|
-
* and destroyed on a cycle by `testerSandboxCleanup`
|
|
20
|
-
* so anything Google indexes from them 404s shortly after. 34 of 182 sitemap
|
|
19
|
+
* and destroyed on a cycle by `testerSandboxCleanup` and by each tester run's
|
|
20
|
+
* wipe/seed, so anything Google indexes from them 404s shortly after. 34 of 182 sitemap
|
|
21
21
|
* URLs were disposable test fixtures before this filter existed.
|
|
22
22
|
*
|
|
23
23
|
* 🛑 This MUST be an in-memory filter, never a Firestore inequality on the
|
|
@@ -8,7 +8,7 @@ export const PUBLIC_SITE_SETTINGS_FIELDS = [
|
|
|
8
8
|
"payment", // CheckoutRouteClient — payment.otpCheckoutThreshold (narrowed)
|
|
9
9
|
"listings", // useListingTypeFlags — narrowed to listings.listingTypes; categoryTypes has no client reader
|
|
10
10
|
"notificationChannels", // NotificationPreferencesPanel + CheckoutRouteClient (whatsapp.otpEnabled)
|
|
11
|
-
"announcementBar", //
|
|
11
|
+
"announcementBar", // the homepage banner (AdminSiteSettingsView edits it)
|
|
12
12
|
"navConfig", // NavbarWithSettings, DashboardLayoutClient
|
|
13
13
|
"actionConfig", // useAuthGate
|
|
14
14
|
"background", // DashboardLayoutClient (background.light / .dark)
|
|
@@ -22,9 +22,8 @@ export declare const cleanupRtdbEvents: import("./types").ScheduledFunctionDefin
|
|
|
22
22
|
export declare const mediaTmpCleanup: import("./types").ScheduledFunctionDefinition;
|
|
23
23
|
export declare const draftPrune: import("./types").ScheduledFunctionDefinition;
|
|
24
24
|
export declare const testerSandboxCleanup: import("./types").ScheduledFunctionDefinition;
|
|
25
|
-
export declare const testerSandboxRefresh: import("./types").ScheduledFunctionDefinition;
|
|
26
25
|
export declare const prizeDrawExpiryReveal: import("./types").ScheduledFunctionDefinition;
|
|
27
26
|
export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
|
|
28
27
|
export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
|
|
29
28
|
export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
|
|
30
|
-
export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition
|
|
29
|
+
export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Scheduled function definitions (Cloud Scheduler triggers).
|
|
2
2
|
// Cron syntax accepts both Firebase shorthand and standard cron expressions.
|
|
3
|
-
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler,
|
|
3
|
+
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, } from "../jobs/handlers";
|
|
4
4
|
import { defineFunction } from "./define";
|
|
5
5
|
const REGION = "asia-south1";
|
|
6
6
|
const EVERY_5_MIN = "every 5 minutes";
|
|
@@ -182,13 +182,13 @@ export const testerSandboxCleanup = defineFunction({
|
|
|
182
182
|
handler: testerSandboxCleanupHandler,
|
|
183
183
|
options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
|
|
184
184
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
185
|
+
// testerSandboxRefresh (every 4h: revert fixtures to seed shape, prune extras) was
|
|
186
|
+
// REMOVED. The tester runner now owns fixture state — it seeds a known catalog at the
|
|
187
|
+
// start of a run and wipes at the end — so a background job independently rewriting
|
|
188
|
+
// those same documents was redundant and, worse, raced a run in progress: it reset
|
|
189
|
+
// currentBid and recomputed auctionEndDate underneath a test that was mid-assertion.
|
|
190
|
+
// Do not reinstate it without also deciding who owns fixture state; two owners is the
|
|
191
|
+
// bug it caused. See tester/scripts/lifecycle.mjs.
|
|
192
192
|
export const prizeDrawExpiryReveal = defineFunction({
|
|
193
193
|
name: "prizeDrawExpiryReveal",
|
|
194
194
|
description: "Assign winners for outstanding paid orders on scheduled-mode prize draws past their expiry, then close them (every 5 minutes).",
|
|
@@ -242,7 +242,6 @@ export const SCHEDULED_FUNCTIONS = [
|
|
|
242
242
|
mediaTmpCleanup,
|
|
243
243
|
draftPrune,
|
|
244
244
|
testerSandboxCleanup,
|
|
245
|
-
testerSandboxRefresh,
|
|
246
245
|
prizeDrawExpiryReveal,
|
|
247
246
|
bundleStockSync,
|
|
248
247
|
emiInstallmentReminder,
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { JobContext } from "../runtime/types";
|
|
2
|
+
/**
|
|
3
|
+
* 🛑 THE definition of "sandbox scope" — six collections, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* Everything OUTSIDE this list is permanent: neither this job nor anything else
|
|
6
|
+
* reclaims it. That is why `users`, `addresses`, `sessions` and `siteSettings`
|
|
7
|
+
* survive a tester run untouched, and why the tester lifecycle's cascade delete
|
|
8
|
+
* (tester/scripts/lib/collections.mjs) mirrors this list rather than inventing
|
|
9
|
+
* its own — a second definition is how the two silently disagree.
|
|
10
|
+
*
|
|
11
|
+
* Adding a collection here makes it disposable. Do not add one without also
|
|
12
|
+
* adding a seed fixture for it, or a run will delete rows nothing recreates.
|
|
13
|
+
*
|
|
14
|
+
* (This used to be shared with testerSandboxRefresh.ts, which was removed when
|
|
15
|
+
* the tester runner took over fixture state — see scheduled.ts.)
|
|
16
|
+
*/
|
|
2
17
|
export declare const SANDBOX_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events", "offers"];
|
|
3
18
|
export interface TesterSandboxCleanupOptions {
|
|
4
19
|
/** true = wipe every isTestData:true doc regardless of testDataExpiresAt (manual force-purge). */
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { batchDelete, getTestDataRefs } from "../handlers/_helpers";
|
|
2
2
|
import { BID_FIELDS } from "../../../../constants/field-names";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/**
|
|
4
|
+
* 🛑 THE definition of "sandbox scope" — six collections, and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* Everything OUTSIDE this list is permanent: neither this job nor anything else
|
|
7
|
+
* reclaims it. That is why `users`, `addresses`, `sessions` and `siteSettings`
|
|
8
|
+
* survive a tester run untouched, and why the tester lifecycle's cascade delete
|
|
9
|
+
* (tester/scripts/lib/collections.mjs) mirrors this list rather than inventing
|
|
10
|
+
* its own — a second definition is how the two silently disagree.
|
|
11
|
+
*
|
|
12
|
+
* Adding a collection here makes it disposable. Do not add one without also
|
|
13
|
+
* adding a seed fixture for it, or a run will delete rows nothing recreates.
|
|
14
|
+
*
|
|
15
|
+
* (This used to be shared with testerSandboxRefresh.ts, which was removed when
|
|
16
|
+
* the tester runner took over fixture state — see scheduled.ts.)
|
|
17
|
+
*/
|
|
9
18
|
export const SANDBOX_COLLECTIONS = ["categories", "stores", "products", "blogPosts", "events", "offers"];
|
|
10
19
|
const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
|
|
11
20
|
export async function runTesterSandboxCleanup(ctx, opts = {}) {
|
|
@@ -54,7 +54,6 @@ export { onScamReportCreateHandler } from "./onScamReportCreate";
|
|
|
54
54
|
export { onScamReportUpdateHandler } from "./onScamReportUpdate";
|
|
55
55
|
export { draftPruneHandler } from "./draftPrune";
|
|
56
56
|
export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
|
|
57
|
-
export { testerSandboxRefreshHandler } from "./testerSandboxRefresh";
|
|
58
57
|
export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
|
|
59
58
|
export { onShipmentItemWriteHandler } from "./onShipmentItemWrite";
|
|
60
59
|
export { onShipmentLotWriteHandler, onShipmentHeaderWriteHandler } from "./onShipmentAllocationSync";
|
|
@@ -67,7 +67,6 @@ export { draftPruneHandler } from "./draftPrune";
|
|
|
67
67
|
// Tester program — 7-day auto-expiry sweep for the shared admin-seeded test sandbox
|
|
68
68
|
export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
|
|
69
69
|
// Tester program — every-4h revert-live-edits + prune-extras refresh (faster than the 7-day sweep above)
|
|
70
|
-
export { testerSandboxRefreshHandler } from "./testerSandboxRefresh";
|
|
71
70
|
// EMI — installment reminder + overdue-flagging sweep
|
|
72
71
|
export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
|
|
73
72
|
// Procurement Shipments — 3-function recompute cascade (item→lot, lot/header→shipment allocation, cascade-delete)
|
|
@@ -22,6 +22,18 @@ export declare const REFUND_COPY: {
|
|
|
22
22
|
readonly submittingLabel: "Submitting…";
|
|
23
23
|
readonly errorFallback: "Failed to submit refund request";
|
|
24
24
|
readonly nonRefundableMessage: "This order is non-refundable (prize draw or bundle). Refunds cannot be issued.";
|
|
25
|
+
/**
|
|
26
|
+
* Shown when a final-sale line is in scope and the chosen reason is a
|
|
27
|
+
* change-of-mind one. Deliberately names the reasons that ARE accepted —
|
|
28
|
+
* a bare "final sale, request refused" reads as a dead end and generates a
|
|
29
|
+
* support ticket, when in most cases the buyer simply picked the wrong
|
|
30
|
+
* reason from the list.
|
|
31
|
+
*/
|
|
32
|
+
readonly finalSaleBlockedMessage: "This order includes a final-sale item, so it can't be returned for a change of mind. If it never arrived, arrived damaged, was the wrong item, wasn't as described, or is counterfeit, choose that reason instead and your request will go through.";
|
|
33
|
+
readonly finalSaleNoticeHeading: "This order includes a final-sale item";
|
|
34
|
+
readonly reasonSelectLabel: "What went wrong?";
|
|
35
|
+
readonly reasonSelectPlaceholder: "Choose a reason";
|
|
36
|
+
readonly reasonNoteLabel: "Anything else we should know? (optional)";
|
|
25
37
|
readonly alreadyRefundedMessage: "A refund has already been processed on this order. No further disputes or refund requests can be filed.";
|
|
26
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 5–7 business days to reflect and that platform fees may be non-recoverable."];
|
|
27
39
|
};
|
|
@@ -22,6 +22,18 @@ export const REFUND_COPY = {
|
|
|
22
22
|
submittingLabel: "Submitting…",
|
|
23
23
|
errorFallback: "Failed to submit refund request",
|
|
24
24
|
nonRefundableMessage: "This order is non-refundable (prize draw or bundle). Refunds cannot be issued.",
|
|
25
|
+
/**
|
|
26
|
+
* Shown when a final-sale line is in scope and the chosen reason is a
|
|
27
|
+
* change-of-mind one. Deliberately names the reasons that ARE accepted —
|
|
28
|
+
* a bare "final sale, request refused" reads as a dead end and generates a
|
|
29
|
+
* support ticket, when in most cases the buyer simply picked the wrong
|
|
30
|
+
* reason from the list.
|
|
31
|
+
*/
|
|
32
|
+
finalSaleBlockedMessage: "This order includes a final-sale item, so it can't be returned for a change of mind. If it never arrived, arrived damaged, was the wrong item, wasn't as described, or is counterfeit, choose that reason instead and your request will go through.",
|
|
33
|
+
finalSaleNoticeHeading: "This order includes a final-sale item",
|
|
34
|
+
reasonSelectLabel: "What went wrong?",
|
|
35
|
+
reasonSelectPlaceholder: "Choose a reason",
|
|
36
|
+
reasonNoteLabel: "Anything else we should know? (optional)",
|
|
25
37
|
alreadyRefundedMessage: "A refund has already been processed on this order. No further disputes or refund requests can be filed.",
|
|
26
38
|
acknowledgments: [
|
|
27
39
|
"I understand this refund request, once approved, permanently removes my ability to file any dispute or RMA claim on this order.",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return / refund reasons, and which of them a FINAL SALE may refuse.
|
|
3
|
+
*
|
|
4
|
+
* ## Why a closed enum and not free text
|
|
5
|
+
*
|
|
6
|
+
* Before this, every return and refund path took `reason: z.string()`. The
|
|
7
|
+
* seller-facing refund routes still recorded it, but `requestReturnAction`
|
|
8
|
+
* parsed it and then dropped it on the floor — the field never reached
|
|
9
|
+
* Firestore. So there was no stored answer to "why was this returned", and
|
|
10
|
+
* nothing a gate could branch on.
|
|
11
|
+
*
|
|
12
|
+
* Final sale needs exactly that branch. "No returns, unless the buyer didn't
|
|
13
|
+
* get what they paid for" is not a policy that can be enforced against a
|
|
14
|
+
* free-text box: it needs a machine-readable statement of what went wrong.
|
|
15
|
+
*
|
|
16
|
+
* ## The split
|
|
17
|
+
*
|
|
18
|
+
* The exempt set is not "serious reasons" — it is **the reasons where the
|
|
19
|
+
* buyer did not receive what the listing promised**. That is a claim about the
|
|
20
|
+
* seller's performance of the contract, and no shop-level policy may waive it.
|
|
21
|
+
* Everything in the blocked set is the buyer changing their mind about a thing
|
|
22
|
+
* they did receive as described, which is exactly what final sale exists to
|
|
23
|
+
* refuse.
|
|
24
|
+
*
|
|
25
|
+
* A reason is EXEMPT if the fault lies with the seller or the carrier.
|
|
26
|
+
* A reason is BLOCKED (under final sale) if the fault lies with nobody.
|
|
27
|
+
*/
|
|
28
|
+
export declare const RETURN_REASON: readonly ["not_received", "not_as_described", "damaged_in_transit", "wrong_item_sent", "counterfeit_or_inauthentic", "changed_mind", "ordered_by_mistake", "found_better_price", "no_longer_needed", "size_or_fit"];
|
|
29
|
+
export type ReturnReason = (typeof RETURN_REASON)[number];
|
|
30
|
+
/**
|
|
31
|
+
* Reasons a final-sale listing can never refuse.
|
|
32
|
+
*
|
|
33
|
+
* 🛑 Adding a member to `RETURN_REASON` does NOT automatically add it here,
|
|
34
|
+
* and that asymmetry is deliberate: a new reason defaults to BLOCKED under
|
|
35
|
+
* final sale, which is the conservative direction — it can be appealed to
|
|
36
|
+
* support, whereas a wrongly-exempt reason silently hands out refunds the
|
|
37
|
+
* seller never agreed to. `RETURN_REASON_LABEL` below is a
|
|
38
|
+
* `Record<ReturnReason, string>` precisely so a new member still cannot be
|
|
39
|
+
* added without a deliberate decision about it.
|
|
40
|
+
*/
|
|
41
|
+
export declare const FINAL_SALE_EXEMPT_REASONS: ReadonlySet<ReturnReason>;
|
|
42
|
+
/**
|
|
43
|
+
* Buyer-facing labels. `Record<ReturnReason, string>`, so a new reason is a
|
|
44
|
+
* COMPILE error here rather than a select option rendering `undefined`.
|
|
45
|
+
*
|
|
46
|
+
* Lives beside the enum rather than in the component, matching `REFUND_COPY` —
|
|
47
|
+
* these strings appear in the buyer form, the admin queue and the notification
|
|
48
|
+
* body, and three copies of them would drift.
|
|
49
|
+
*/
|
|
50
|
+
export declare const RETURN_REASON_LABEL: Record<ReturnReason, string>;
|
|
51
|
+
/** Short grouping label, used to head the two option groups in the picker. */
|
|
52
|
+
export declare const RETURN_REASON_GROUP_LABEL: {
|
|
53
|
+
readonly fault: "Something was wrong with the order";
|
|
54
|
+
readonly changeOfMind: "I changed my mind";
|
|
55
|
+
};
|
|
56
|
+
export declare function isFinalSaleExempt(reason: ReturnReason): boolean;
|
|
57
|
+
export declare function isReturnReason(value: unknown): value is ReturnReason;
|
|
58
|
+
/** The reasons a buyer may pick for this order, given its final-sale lines. */
|
|
59
|
+
export declare function selectableReturnReasons(hasFinalSaleLine: boolean): readonly ReturnReason[];
|