@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
|
@@ -0,0 +1,96 @@
|
|
|
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 const RETURN_REASON = [
|
|
29
|
+
// ── Seller / carrier fault. Never blocked, whatever the listing says. ──
|
|
30
|
+
"not_received",
|
|
31
|
+
"not_as_described",
|
|
32
|
+
"damaged_in_transit",
|
|
33
|
+
"wrong_item_sent",
|
|
34
|
+
"counterfeit_or_inauthentic",
|
|
35
|
+
// ── Change of mind. Blocked when the line is final sale. ──
|
|
36
|
+
"changed_mind",
|
|
37
|
+
"ordered_by_mistake",
|
|
38
|
+
"found_better_price",
|
|
39
|
+
"no_longer_needed",
|
|
40
|
+
"size_or_fit",
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* Reasons a final-sale listing can never refuse.
|
|
44
|
+
*
|
|
45
|
+
* 🛑 Adding a member to `RETURN_REASON` does NOT automatically add it here,
|
|
46
|
+
* and that asymmetry is deliberate: a new reason defaults to BLOCKED under
|
|
47
|
+
* final sale, which is the conservative direction — it can be appealed to
|
|
48
|
+
* support, whereas a wrongly-exempt reason silently hands out refunds the
|
|
49
|
+
* seller never agreed to. `RETURN_REASON_LABEL` below is a
|
|
50
|
+
* `Record<ReturnReason, string>` precisely so a new member still cannot be
|
|
51
|
+
* added without a deliberate decision about it.
|
|
52
|
+
*/
|
|
53
|
+
export const FINAL_SALE_EXEMPT_REASONS = new Set([
|
|
54
|
+
"not_received",
|
|
55
|
+
"not_as_described",
|
|
56
|
+
"damaged_in_transit",
|
|
57
|
+
"wrong_item_sent",
|
|
58
|
+
"counterfeit_or_inauthentic",
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* Buyer-facing labels. `Record<ReturnReason, string>`, so a new reason is a
|
|
62
|
+
* COMPILE error here rather than a select option rendering `undefined`.
|
|
63
|
+
*
|
|
64
|
+
* Lives beside the enum rather than in the component, matching `REFUND_COPY` —
|
|
65
|
+
* these strings appear in the buyer form, the admin queue and the notification
|
|
66
|
+
* body, and three copies of them would drift.
|
|
67
|
+
*/
|
|
68
|
+
export const RETURN_REASON_LABEL = {
|
|
69
|
+
not_received: "It never arrived",
|
|
70
|
+
not_as_described: "Not as described in the listing",
|
|
71
|
+
damaged_in_transit: "Arrived damaged",
|
|
72
|
+
wrong_item_sent: "The wrong item was sent",
|
|
73
|
+
counterfeit_or_inauthentic: "Counterfeit or not authentic",
|
|
74
|
+
changed_mind: "I changed my mind",
|
|
75
|
+
ordered_by_mistake: "I ordered it by mistake",
|
|
76
|
+
found_better_price: "I found a better price elsewhere",
|
|
77
|
+
no_longer_needed: "I no longer need it",
|
|
78
|
+
size_or_fit: "Size or fit is wrong",
|
|
79
|
+
};
|
|
80
|
+
/** Short grouping label, used to head the two option groups in the picker. */
|
|
81
|
+
export const RETURN_REASON_GROUP_LABEL = {
|
|
82
|
+
fault: "Something was wrong with the order",
|
|
83
|
+
changeOfMind: "I changed my mind",
|
|
84
|
+
};
|
|
85
|
+
export function isFinalSaleExempt(reason) {
|
|
86
|
+
return FINAL_SALE_EXEMPT_REASONS.has(reason);
|
|
87
|
+
}
|
|
88
|
+
export function isReturnReason(value) {
|
|
89
|
+
return typeof value === "string" && RETURN_REASON.includes(value);
|
|
90
|
+
}
|
|
91
|
+
/** The reasons a buyer may pick for this order, given its final-sale lines. */
|
|
92
|
+
export function selectableReturnReasons(hasFinalSaleLine) {
|
|
93
|
+
return hasFinalSaleLine
|
|
94
|
+
? RETURN_REASON.filter(isFinalSaleExempt)
|
|
95
|
+
: RETURN_REASON;
|
|
96
|
+
}
|
|
@@ -127,6 +127,36 @@ export declare const cancelOrderSchema: z.ZodObject<{
|
|
|
127
127
|
orderId: string;
|
|
128
128
|
reason?: string | undefined;
|
|
129
129
|
}>;
|
|
130
|
+
/**
|
|
131
|
+
* A buyer asking to return a delivered order.
|
|
132
|
+
*
|
|
133
|
+
* `requestReturnAction` used to parse `cancelOrderSchema` — a cancellation is
|
|
134
|
+
* a different event with a different window and a different outcome, and its
|
|
135
|
+
* `reason` was optional free text that the action then dropped before writing.
|
|
136
|
+
* So an order could reach `return_requested` carrying no reason at all, and
|
|
137
|
+
* the seller had nothing to act on.
|
|
138
|
+
*
|
|
139
|
+
* The code is REQUIRED here: final sale has to decide whether this particular
|
|
140
|
+
* return is allowed, and it cannot do that against an absent value.
|
|
141
|
+
*/
|
|
142
|
+
export declare const returnRequestSchema: z.ZodObject<{
|
|
143
|
+
orderId: z.ZodString;
|
|
144
|
+
reasonCode: z.ZodEnum<["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"]>;
|
|
145
|
+
reasonNote: z.ZodOptional<z.ZodString>;
|
|
146
|
+
/** Omit for a whole-order return — matches `processRefundAction`'s itemIds. */
|
|
147
|
+
itemIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
148
|
+
}, "strip", z.ZodTypeAny, {
|
|
149
|
+
orderId: string;
|
|
150
|
+
reasonCode: "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";
|
|
151
|
+
itemIds?: string[] | undefined;
|
|
152
|
+
reasonNote?: string | undefined;
|
|
153
|
+
}, {
|
|
154
|
+
orderId: string;
|
|
155
|
+
reasonCode: "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";
|
|
156
|
+
itemIds?: string[] | undefined;
|
|
157
|
+
reasonNote?: string | undefined;
|
|
158
|
+
}>;
|
|
130
159
|
export type CreateOrderInput = z.infer<typeof createOrderSchema>;
|
|
131
160
|
export type UpdateOrderStatusInput = z.infer<typeof updateOrderStatusSchema>;
|
|
132
161
|
export type CancelOrderInput = z.infer<typeof cancelOrderSchema>;
|
|
162
|
+
export type ReturnRequestInput = z.infer<typeof returnRequestSchema>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { DEFAULT_COUNTRY, isValidPostalCode, postalLabelFor, } from "../../../../constants/geo/countries";
|
|
3
3
|
import { ORDER_CANCEL_REASON_MAX_LENGTH, ORDER_NOTE_MAX_LENGTH, ORDER_TRACKING_NUMBER_MAX_LENGTH, } from "./config";
|
|
4
|
+
import { RETURN_REASON } from "./return-reasons";
|
|
4
5
|
/*
|
|
5
6
|
* The order's own SNAPSHOT of where it shipped, denormalised onto the order
|
|
6
7
|
* document at creation. Its field names (`pincode`) are stored on every
|
|
@@ -51,3 +52,22 @@ export const cancelOrderSchema = z.object({
|
|
|
51
52
|
orderId: z.string().min(1),
|
|
52
53
|
reason: z.string().max(ORDER_CANCEL_REASON_MAX_LENGTH).optional(),
|
|
53
54
|
});
|
|
55
|
+
/**
|
|
56
|
+
* A buyer asking to return a delivered order.
|
|
57
|
+
*
|
|
58
|
+
* `requestReturnAction` used to parse `cancelOrderSchema` — a cancellation is
|
|
59
|
+
* a different event with a different window and a different outcome, and its
|
|
60
|
+
* `reason` was optional free text that the action then dropped before writing.
|
|
61
|
+
* So an order could reach `return_requested` carrying no reason at all, and
|
|
62
|
+
* the seller had nothing to act on.
|
|
63
|
+
*
|
|
64
|
+
* The code is REQUIRED here: final sale has to decide whether this particular
|
|
65
|
+
* return is allowed, and it cannot do that against an absent value.
|
|
66
|
+
*/
|
|
67
|
+
export const returnRequestSchema = z.object({
|
|
68
|
+
orderId: z.string().min(1),
|
|
69
|
+
reasonCode: z.enum(RETURN_REASON),
|
|
70
|
+
reasonNote: z.string().max(ORDER_CANCEL_REASON_MAX_LENGTH).optional(),
|
|
71
|
+
/** Omit for a whole-order return — matches `processRefundAction`'s itemIds. */
|
|
72
|
+
itemIds: z.array(z.string().min(1)).optional(),
|
|
73
|
+
});
|
|
@@ -17,6 +17,7 @@ export declare const productInputSchema: z.ZodObject<{
|
|
|
17
17
|
seoKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
18
18
|
shippingInfo: z.ZodOptional<z.ZodString>;
|
|
19
19
|
returnPolicy: z.ZodOptional<z.ZodString>;
|
|
20
|
+
finalSale: z.ZodOptional<z.ZodBoolean>;
|
|
20
21
|
shippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>;
|
|
21
22
|
insurance: z.ZodOptional<z.ZodBoolean>;
|
|
22
23
|
insuranceCost: z.ZodOptional<z.ZodNumber>;
|
|
@@ -45,6 +46,7 @@ export declare const productInputSchema: z.ZodObject<{
|
|
|
45
46
|
seoTitle?: string | undefined;
|
|
46
47
|
seoDescription?: string | undefined;
|
|
47
48
|
seoKeywords?: string[] | undefined;
|
|
49
|
+
finalSale?: boolean | undefined;
|
|
48
50
|
allowOffers?: boolean | undefined;
|
|
49
51
|
minOfferPercent?: number | undefined;
|
|
50
52
|
}, {
|
|
@@ -69,6 +71,7 @@ export declare const productInputSchema: z.ZodObject<{
|
|
|
69
71
|
seoTitle?: string | undefined;
|
|
70
72
|
seoDescription?: string | undefined;
|
|
71
73
|
seoKeywords?: string[] | undefined;
|
|
74
|
+
finalSale?: boolean | undefined;
|
|
72
75
|
allowOffers?: boolean | undefined;
|
|
73
76
|
minOfferPercent?: number | undefined;
|
|
74
77
|
}>;
|
|
@@ -90,6 +93,7 @@ export declare const productUpdateSchema: z.ZodObject<{
|
|
|
90
93
|
seoKeywords: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
91
94
|
shippingInfo: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
92
95
|
returnPolicy: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
96
|
+
finalSale: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
93
97
|
shippingPaidBy: z.ZodOptional<z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>>;
|
|
94
98
|
insurance: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
95
99
|
insuranceCost: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
@@ -118,6 +122,7 @@ export declare const productUpdateSchema: z.ZodObject<{
|
|
|
118
122
|
seoTitle?: string | undefined;
|
|
119
123
|
seoDescription?: string | undefined;
|
|
120
124
|
seoKeywords?: string[] | undefined;
|
|
125
|
+
finalSale?: boolean | undefined;
|
|
121
126
|
allowOffers?: boolean | undefined;
|
|
122
127
|
minOfferPercent?: number | undefined;
|
|
123
128
|
}, {
|
|
@@ -142,6 +147,7 @@ export declare const productUpdateSchema: z.ZodObject<{
|
|
|
142
147
|
seoTitle?: string | undefined;
|
|
143
148
|
seoDescription?: string | undefined;
|
|
144
149
|
seoKeywords?: string[] | undefined;
|
|
150
|
+
finalSale?: boolean | undefined;
|
|
145
151
|
allowOffers?: boolean | undefined;
|
|
146
152
|
minOfferPercent?: number | undefined;
|
|
147
153
|
}>;
|
|
@@ -163,6 +169,7 @@ export declare const auctionInputSchema: z.ZodObject<{
|
|
|
163
169
|
seoKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
164
170
|
shippingInfo: z.ZodOptional<z.ZodString>;
|
|
165
171
|
returnPolicy: z.ZodOptional<z.ZodString>;
|
|
172
|
+
finalSale: z.ZodOptional<z.ZodBoolean>;
|
|
166
173
|
shippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>;
|
|
167
174
|
insurance: z.ZodOptional<z.ZodBoolean>;
|
|
168
175
|
insuranceCost: z.ZodOptional<z.ZodNumber>;
|
|
@@ -210,6 +217,7 @@ export declare const auctionInputSchema: z.ZodObject<{
|
|
|
210
217
|
seoTitle?: string | undefined;
|
|
211
218
|
seoDescription?: string | undefined;
|
|
212
219
|
seoKeywords?: string[] | undefined;
|
|
220
|
+
finalSale?: boolean | undefined;
|
|
213
221
|
allowOffers?: boolean | undefined;
|
|
214
222
|
minOfferPercent?: number | undefined;
|
|
215
223
|
}, {
|
|
@@ -243,6 +251,7 @@ export declare const auctionInputSchema: z.ZodObject<{
|
|
|
243
251
|
seoTitle?: string | undefined;
|
|
244
252
|
seoDescription?: string | undefined;
|
|
245
253
|
seoKeywords?: string[] | undefined;
|
|
254
|
+
finalSale?: boolean | undefined;
|
|
246
255
|
allowOffers?: boolean | undefined;
|
|
247
256
|
minOfferPercent?: number | undefined;
|
|
248
257
|
}>;
|
|
@@ -264,6 +273,7 @@ export declare const preOrderInputSchema: z.ZodObject<{
|
|
|
264
273
|
seoKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
265
274
|
shippingInfo: z.ZodOptional<z.ZodString>;
|
|
266
275
|
returnPolicy: z.ZodOptional<z.ZodString>;
|
|
276
|
+
finalSale: z.ZodOptional<z.ZodBoolean>;
|
|
267
277
|
shippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>;
|
|
268
278
|
insurance: z.ZodOptional<z.ZodBoolean>;
|
|
269
279
|
insuranceCost: z.ZodOptional<z.ZodNumber>;
|
|
@@ -305,6 +315,7 @@ export declare const preOrderInputSchema: z.ZodObject<{
|
|
|
305
315
|
seoTitle?: string | undefined;
|
|
306
316
|
seoDescription?: string | undefined;
|
|
307
317
|
seoKeywords?: string[] | undefined;
|
|
318
|
+
finalSale?: boolean | undefined;
|
|
308
319
|
allowOffers?: boolean | undefined;
|
|
309
320
|
minOfferPercent?: number | undefined;
|
|
310
321
|
}, {
|
|
@@ -335,6 +346,7 @@ export declare const preOrderInputSchema: z.ZodObject<{
|
|
|
335
346
|
seoTitle?: string | undefined;
|
|
336
347
|
seoDescription?: string | undefined;
|
|
337
348
|
seoKeywords?: string[] | undefined;
|
|
349
|
+
finalSale?: boolean | undefined;
|
|
338
350
|
allowOffers?: boolean | undefined;
|
|
339
351
|
minOfferPercent?: number | undefined;
|
|
340
352
|
}>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { mediaUrlSchema } from "../../../../validation/schemas";
|
|
3
|
+
import { PRODUCT_MAX_IMAGES } from "../../media/limits";
|
|
3
4
|
const priceSchema = z.number().min(0, "Price must be non-negative");
|
|
4
5
|
const conditionSchema = z.enum(["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]);
|
|
5
6
|
export const productInputSchema = z.object({
|
|
@@ -12,7 +13,7 @@ export const productInputSchema = z.object({
|
|
|
12
13
|
stockQuantity: z.number().int().min(0).default(0),
|
|
13
14
|
availableQuantity: z.number().int().min(0).default(0),
|
|
14
15
|
mainImage: mediaUrlSchema,
|
|
15
|
-
images: z.array(mediaUrlSchema).max(
|
|
16
|
+
images: z.array(mediaUrlSchema).max(PRODUCT_MAX_IMAGES).default([]),
|
|
16
17
|
condition: conditionSchema.optional(),
|
|
17
18
|
tags: z.array(z.string()).max(20).default([]),
|
|
18
19
|
seoTitle: z.string().max(70).optional(),
|
|
@@ -20,6 +21,8 @@ export const productInputSchema = z.object({
|
|
|
20
21
|
seoKeywords: z.array(z.string()).max(10).optional(),
|
|
21
22
|
shippingInfo: z.string().max(1000).optional(),
|
|
22
23
|
returnPolicy: z.string().max(1000).optional(),
|
|
24
|
+
// Absent means final sale. No `.default()` — see request-schemas.ts.
|
|
25
|
+
finalSale: z.boolean().optional(),
|
|
23
26
|
shippingPaidBy: z.enum(["seller", "buyer"]).optional(),
|
|
24
27
|
insurance: z.boolean().optional(),
|
|
25
28
|
insuranceCost: z.number().min(0).optional(),
|
|
@@ -13,6 +13,20 @@ export declare const MAX_IMAGE_BYTES: number;
|
|
|
13
13
|
export declare const MAX_PDF_BYTES: number;
|
|
14
14
|
export declare const MAX_VIDEO_BYTES: number;
|
|
15
15
|
export type MediaKind = "image" | "video" | "pdf";
|
|
16
|
+
/** Gallery images per listing. */
|
|
17
|
+
export declare const PRODUCT_MAX_IMAGES = 10;
|
|
18
|
+
/** Videos per listing. Bounded by `ProductDocument.video` being singular. */
|
|
19
|
+
export declare const PRODUCT_MAX_VIDEOS = 1;
|
|
20
|
+
/**
|
|
21
|
+
* Highest legal `index` in a product media filename.
|
|
22
|
+
*
|
|
23
|
+
* NOT the same number as `PRODUCT_MAX_IMAGES`, and conflating them is a live
|
|
24
|
+
* bug: `generateMediaFilename` gives the main image index 1 and the gallery
|
|
25
|
+
* 2..N, so the index space is one larger than the gallery count. The server
|
|
26
|
+
* guard checks the INDEX, which is why a gallery capped at 5 was rejecting its
|
|
27
|
+
* own 5th image with a 400 (it arrived as index 6).
|
|
28
|
+
*/
|
|
29
|
+
export declare const PRODUCT_IMAGE_INDEX_MAX: number;
|
|
16
30
|
export declare const MAX_LABEL: Record<MediaKind, string>;
|
|
17
31
|
export declare const MAX_BYTES: Record<MediaKind, number>;
|
|
18
32
|
export declare const ALLOWED_IMAGE_MIMES: readonly ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"];
|
|
@@ -12,6 +12,35 @@ export const MEGABYTE = 1024 * 1024;
|
|
|
12
12
|
export const MAX_IMAGE_BYTES = 10 * MEGABYTE;
|
|
13
13
|
export const MAX_PDF_BYTES = 20 * MEGABYTE;
|
|
14
14
|
export const MAX_VIDEO_BYTES = 50 * MEGABYTE;
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Per-listing media COUNTS.
|
|
17
|
+
//
|
|
18
|
+
// Byte ceilings are above; these are how MANY files one listing may carry.
|
|
19
|
+
// Deliberately FLAT — every listing type gets the same allowance. There is no
|
|
20
|
+
// per-type table and one should not be added: the previous state had six
|
|
21
|
+
// disagreeing numbers (5 in the request schema, 5 in the seller form, 5 in the
|
|
22
|
+
// admin form, 5/5/5 in the server context guards, 12 in the MediaUploadList
|
|
23
|
+
// default, 20 in the internal product schema) and no two of them were reached
|
|
24
|
+
// by the same code path, so which cap a seller actually hit depended on which
|
|
25
|
+
// form they had opened.
|
|
26
|
+
//
|
|
27
|
+
// The video cap is 1 because `ProductDocument.video` is a single field, not an
|
|
28
|
+
// array — raising it is a schema change, not a constant change.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/** Gallery images per listing. */
|
|
31
|
+
export const PRODUCT_MAX_IMAGES = 10;
|
|
32
|
+
/** Videos per listing. Bounded by `ProductDocument.video` being singular. */
|
|
33
|
+
export const PRODUCT_MAX_VIDEOS = 1;
|
|
34
|
+
/**
|
|
35
|
+
* Highest legal `index` in a product media filename.
|
|
36
|
+
*
|
|
37
|
+
* NOT the same number as `PRODUCT_MAX_IMAGES`, and conflating them is a live
|
|
38
|
+
* bug: `generateMediaFilename` gives the main image index 1 and the gallery
|
|
39
|
+
* 2..N, so the index space is one larger than the gallery count. The server
|
|
40
|
+
* guard checks the INDEX, which is why a gallery capped at 5 was rejecting its
|
|
41
|
+
* own 5th image with a 400 (it arrived as index 6).
|
|
42
|
+
*/
|
|
43
|
+
export const PRODUCT_IMAGE_INDEX_MAX = PRODUCT_MAX_IMAGES + 1;
|
|
15
44
|
export const MAX_LABEL = {
|
|
16
45
|
image: "10MB",
|
|
17
46
|
video: "50MB",
|
package/dist/client.d.ts
CHANGED
|
@@ -383,6 +383,7 @@ export type { GuestHistoryItem, GuestHistoryType, UserHistoryItem, HistoryProduc
|
|
|
383
383
|
export type { TrackArgs as TrackHistoryArgs } from "./features/history/hooks/useHistory";
|
|
384
384
|
export { WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, } from "./constants/limits";
|
|
385
385
|
export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/utils/listing-type";
|
|
386
|
+
export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
|
|
386
387
|
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
|
|
387
388
|
export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
|
|
388
389
|
export { cartRequiresShipping, cartIsDigitalOnly, cartIsChatOnly, } from "./_internal/shared/listing-types/cart-shipping";
|
|
@@ -397,7 +398,7 @@ export type { ListingTypeCapability } from "./_internal/shared/listing-types/cap
|
|
|
397
398
|
export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_internal/shared/listing-types/_registry";
|
|
398
399
|
export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
|
|
399
400
|
export type { ListingType } from "./features/products/types/index";
|
|
400
|
-
export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
|
|
401
|
+
export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
|
|
401
402
|
export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
|
|
402
403
|
export { ScamAwarenessModal } from "./features/scams/components/ScamAwarenessModal";
|
|
403
404
|
export type { ScamAwarenessModalProps } from "./features/scams/components/ScamAwarenessModal";
|
|
@@ -514,3 +515,10 @@ export { AdminActionIndexView } from "./features/admin/components/AdminActionInd
|
|
|
514
515
|
export type { AdminActionIndexViewProps } from "./features/admin/components/AdminActionIndexView";
|
|
515
516
|
export { SellerSupportView } from "./features/seller/components/SellerSupportView";
|
|
516
517
|
export type { SellerSupportViewProps } from "./features/seller/components/SellerSupportView";
|
|
518
|
+
export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
|
|
519
|
+
export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
|
|
520
|
+
export type { ReturnReason } from "./_internal/shared/features/orders/return-reasons";
|
|
521
|
+
export { RefundRequestView } from "./features/orders/components/RefundRequestView";
|
|
522
|
+
export type { RefundRequestViewProps, RefundRequestSubmission, } from "./features/orders/components/RefundRequestView";
|
|
523
|
+
export { ORDER_RETURN_WINDOW_DAYS, ORDER_RETURN_WINDOW_MS, ORDER_CANCELLABLE_STATUSES, } from "./_internal/shared/features/orders/config";
|
|
524
|
+
export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
|
package/dist/client.js
CHANGED
|
@@ -390,6 +390,9 @@ export { WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, } from "./constants/limits";
|
|
|
390
390
|
// SB1-G canonical listing-type accessors (pure functions, client-safe).
|
|
391
391
|
// SB-UNI-F 2026-05-13 — Phase 2 predicates surfaced through client barrel.
|
|
392
392
|
export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/utils/listing-type";
|
|
393
|
+
// Final sale — ABSENT MEANS TRUE, so these are the only legal reads of the
|
|
394
|
+
// `finalSale` field. See the module docstring for why `!p.finalSale` is wrong.
|
|
395
|
+
export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
|
|
393
396
|
// SB-UNI-X4 2026-05-13 — per-type feature-flag helpers (client-safe).
|
|
394
397
|
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
|
|
395
398
|
// SB-UNI-X5 2026-05-13 — action telemetry sink (client-safe; defaults to
|
|
@@ -409,7 +412,7 @@ export { DashboardLayoutClient, RoleGuard } from "./_internal/client/features/la
|
|
|
409
412
|
export { LISTING_TYPE_CAPABILITIES, capabilityFor, canAddToCart, canBid, supportsShipping, requiresVendorVerified, requiresJurisdictionCheck, hasInstantFulfillment, assertNever, } from "./_internal/shared/listing-types/capabilities";
|
|
410
413
|
export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_internal/shared/listing-types/_registry";
|
|
411
414
|
// Media upload limits — shared by client uploaders + server sign/finalize routes.
|
|
412
|
-
export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
|
|
415
|
+
export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
|
|
413
416
|
export { ScamAwarenessModal } from "./features/scams/components/ScamAwarenessModal";
|
|
414
417
|
export { SellerTrustBadge } from "./features/scams/components/SellerTrustBadge";
|
|
415
418
|
// [CLIENT-ONLY] — Admin panel components for action/nav permission management.
|
|
@@ -511,3 +514,18 @@ export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, }
|
|
|
511
514
|
export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
|
|
512
515
|
export { AdminActionIndexView } from "./features/admin/components/AdminActionIndexView";
|
|
513
516
|
export { SellerSupportView } from "./features/seller/components/SellerSupportView";
|
|
517
|
+
export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
|
|
518
|
+
// Return / refund reasons — the closed enum the final-sale gate branches on.
|
|
519
|
+
// See the module docstring for why the exempt set is "seller or carrier fault"
|
|
520
|
+
// rather than "serious reasons".
|
|
521
|
+
export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
|
|
522
|
+
// Buyer-facing return/refund request form and the window it is gated on.
|
|
523
|
+
// Exported from the CLIENT entry because the page that renders it is a
|
|
524
|
+
// "use client" route — importing it from the main entry there would drag the
|
|
525
|
+
// server graph into the browser bundle (Root Cause #6).
|
|
526
|
+
export { RefundRequestView } from "./features/orders/components/RefundRequestView";
|
|
527
|
+
export { ORDER_RETURN_WINDOW_DAYS, ORDER_RETURN_WINDOW_MS, ORDER_CANCELLABLE_STATUSES, } from "./_internal/shared/features/orders/config";
|
|
528
|
+
// Seller listing form draft <-> product write payload. The flat form keys and
|
|
529
|
+
// the nested document blocks are different shapes, and nothing translated
|
|
530
|
+
// between them — see the module docstring.
|
|
531
|
+
export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
|
|
@@ -7,6 +7,7 @@ import { useQuery } from "@tanstack/react-query";
|
|
|
7
7
|
import { Button, Card, CardBody, ConfirmDeleteModal, Div, PaginatedSelect, Stack, StackedViewShell, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast, Show, StickyToolbar, } from "../../../ui";
|
|
8
8
|
import { useFormShellState, applyZodIssues, FormErrorSummary, FormShellContext } from "../../../ui/forms";
|
|
9
9
|
import { SectionForm, useSectionFormNav } from "../../shell";
|
|
10
|
+
import { PRODUCT_MAX_IMAGES } from "../../../_internal/shared/media/limits";
|
|
10
11
|
// Admin form-input validation — deliberately a small local schema, not the
|
|
11
12
|
// broader `productItemSchema` (that one requires `id`/`price`/`status` as
|
|
12
13
|
// non-optional, since it's the full read/display shape used across many
|
|
@@ -15,8 +16,12 @@ import { SectionForm, useSectionFormNav } from "../../shell";
|
|
|
15
16
|
const adminProductFormSchema = z.object({
|
|
16
17
|
title: z.string().min(1, "Product title is required"),
|
|
17
18
|
price: z.number().min(0).optional(),
|
|
18
|
-
images: z
|
|
19
|
+
images: z
|
|
20
|
+
.array(z.string())
|
|
21
|
+
.max(PRODUCT_MAX_IMAGES, `Up to ${PRODUCT_MAX_IMAGES} gallery images allowed`)
|
|
22
|
+
.optional(),
|
|
19
23
|
video: z.object({ url: z.string(), thumbnailUrl: z.string().optional() }).optional(),
|
|
24
|
+
finalSale: z.boolean().optional(),
|
|
20
25
|
}).passthrough();
|
|
21
26
|
import { apiClient } from "../../../http";
|
|
22
27
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
@@ -251,8 +251,6 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
251
251
|
const [settlementGrace, setSettlementGrace] = React.useState(24);
|
|
252
252
|
// ⑪ Platform Limits
|
|
253
253
|
const [maxProductsPerStore, setMaxProductsPerStore] = React.useState(100);
|
|
254
|
-
const [maxImagesPerProduct, setMaxImagesPerProduct] = React.useState(10);
|
|
255
|
-
const [maxVideoSizeMb, setMaxVideoSizeMb] = React.useState(100);
|
|
256
254
|
const [maxCustomFields, setMaxCustomFields] = React.useState(50);
|
|
257
255
|
const [maxCustomSections, setMaxCustomSections] = React.useState(3);
|
|
258
256
|
const [orderCancelWindow, setOrderCancelWindow] = React.useState(24);
|
|
@@ -443,8 +441,6 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
443
441
|
setAutoExtendWindow(s.auctionConfig?.autoExtendWindowMinutes ?? 5);
|
|
444
442
|
setSettlementGrace(s.auctionConfig?.settlementGracePeriodHours ?? 24);
|
|
445
443
|
setMaxProductsPerStore(s.platformLimits?.maxProductsPerStore ?? 100);
|
|
446
|
-
setMaxImagesPerProduct(s.platformLimits?.maxImagesPerProduct ?? 10);
|
|
447
|
-
setMaxVideoSizeMb(s.platformLimits?.maxVideoSizeMb ?? 100);
|
|
448
444
|
setMaxCustomFields(s.platformLimits?.maxCustomFieldsPerProduct ?? 50);
|
|
449
445
|
setMaxCustomSections(s.platformLimits?.maxCustomSectionsPerProduct ?? 3);
|
|
450
446
|
setOrderCancelWindow(s.platformLimits?.orderCancellationWindowHours ?? 24);
|
|
@@ -641,7 +637,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
641
637
|
shipping: { freeShippingThreshold, defaultCarrier, maxDeliveryRadius },
|
|
642
638
|
payment: { razorpayEnabled, upiManualEnabled, codEnabled, otpCheckoutThreshold },
|
|
643
639
|
auctionConfig: { bidIncrementTiers: bidIncrementTiers, autoExtendWindowMinutes: autoExtendWindow, settlementGracePeriodHours: settlementGrace },
|
|
644
|
-
platformLimits: { maxProductsPerStore,
|
|
640
|
+
platformLimits: { maxProductsPerStore, maxCustomFieldsPerProduct: maxCustomFields, maxCustomSectionsPerProduct: maxCustomSections, orderCancellationWindowHours: orderCancelWindow },
|
|
645
641
|
legalPages: { terms: termsHtml, privacy: privacyHtml, refundPolicy: refundHtml, shipping: shippingPolicyHtml, cookies: cookieHtml, ethics: ethicsHtml, codeOfConduct: conductHtml },
|
|
646
642
|
// Both keys are always sent: Firestore's update() replaces a nested map
|
|
647
643
|
// wholesale, so omitting one here would wipe it. The load-time spread this
|
|
@@ -748,7 +744,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
748
744
|
...bidIncrementTiers.slice(0, -1),
|
|
749
745
|
{ upTo: 100, increment: 10 },
|
|
750
746
|
bidIncrementTiers[bidIncrementTiers.length - 1] ?? { upTo: null, increment: 1000 },
|
|
751
|
-
]), children: "+ Add tier" })] }), _jsx(Input, { label: "Auto-extend window (minutes before end)", value: String(autoExtendWindow), onChange: (e) => setAutoExtendWindow(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Extend auction end time if a bid arrives within this window." }), _jsx(Input, { label: "Settlement grace period (hours)", value: String(settlementGrace), onChange: (e) => setSettlementGrace(parseInt(e.target.value) || 0), type: "number", min: 1, helperText: "Time winner has to pay before the auction is re-listed." })] }) }), _jsx(TabsContent, { value: "limits", children: _jsx(SettingsTabForm, { id: "limits", label: "Limits", children: _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Max products per store", value: String(maxProductsPerStore), onChange: (e) => setMaxProductsPerStore(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max images per product", value: String(maxImagesPerProduct), onChange: (e) => setMaxImagesPerProduct(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max video size (MB)", value: String(maxVideoSizeMb), onChange: (e) => setMaxVideoSizeMb(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max custom fields per product", value: String(maxCustomFields), onChange: (e) => setMaxCustomFields(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Max custom sections per product", value: String(maxCustomSections), onChange: (e) => setMaxCustomSections(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Order cancellation window (hours)", value: String(orderCancelWindow), onChange: (e) => setOrderCancelWindow(parseInt(e.target.value) || 0), type: "number", min: 0 })] }) }) }), _jsx(TabsContent, { value: "whatsapp", children: _jsxs(SettingsTabForm, { id: "whatsapp", label: "Whatsapp", children: [_jsx(Text, { size: "xs", color: "muted", children: "Platform-level WhatsApp Business Cloud API credentials. Used for automated purchase announcements to admin numbers when orders are placed. Store owners configure their own credentials in Store \u2192 WhatsApp." }), _jsx(Input, { label: "Phone Number ID", value: waPhoneNumberId, onChange: (e) => setWaPhoneNumberId(e.target.value), placeholder: "987654321098765", helperText: "From Meta for Developers \u2192 App \u2192 WhatsApp \u2192 API Setup" }), _jsx(MaskedInput, { label: "Cloud API System User Token", value: waCloudApiToken, onChange: setWaCloudApiToken, placeholder: "EAAxxxxxxxx\u2026", helperText: "Long-lived system user access token with WhatsApp Business permissions" }), _jsx(Input, { label: "Admin Notify Numbers", value: waAdminNotifyNumbers, onChange: (e) => setWaAdminNotifyNumbers(e.target.value), placeholder: "919876543210,918765432109", helperText: "Comma-separated, digits-only, include country code. These receive a WhatsApp message when any order is placed." }), _jsx(Text, { size: "xs", color: "muted", children: "Approved Meta message templates for the buyer-facing WhatsApp order-updates addon (Fees tab). Meta rejects business-initiated free-text WhatsApp messages outside a 24h customer-service window \u2014 a real template name here is required for that notification type to actually deliver. Leave blank to skip WhatsApp for that type (in-app + email still fire)." }), _jsx(Input, { label: "Template language code", value: waTemplateLanguage, onChange: (e) => setWaTemplateLanguage(e.target.value), placeholder: "en", helperText: "BCP-47 code the templates below were approved in, e.g. en or en_US." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Order placed template name", value: waTemplateOrderPlaced, onChange: (e) => setWaTemplateOrderPlaced(e.target.value), placeholder: "order_placed_update" }), _jsx(Input, { label: "Order confirmed template name", value: waTemplateOrderConfirmed, onChange: (e) => setWaTemplateOrderConfirmed(e.target.value), placeholder: "order_confirmed_update" }), _jsx(Input, { label: "Order shipped template name", value: waTemplateOrderShipped, onChange: (e) => setWaTemplateOrderShipped(e.target.value), placeholder: "order_shipped_update" }), _jsx(Input, { label: "Order delivered template name", value: waTemplateOrderDelivered, onChange: (e) => setWaTemplateOrderDelivered(e.target.value), placeholder: "order_delivered_update" }), _jsx(Input, { label: "Order cancelled template name", value: waTemplateOrderCancelled, onChange: (e) => setWaTemplateOrderCancelled(e.target.value), placeholder: "order_cancelled_update" }), _jsx(Input, { label: "Refund initiated template name", value: waTemplateRefundInitiated, onChange: (e) => setWaTemplateRefundInitiated(e.target.value), placeholder: "refund_initiated_update" })] })] }) }), _jsx(TabsContent, { value: "notifications", children: _jsxs(SettingsTabForm, { id: "notifications", label: "Notifications", children: [_jsx(Text, { size: "xs", color: "muted", children: "In-app notifications are always on. Enable external channels below to let the platform fan out to email, WhatsApp, or SMS. Users can further restrict which types they receive." }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "In-app (notification bell)" }), _jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "semibold", rounded: "full", padding: "pill-xs", children: "Always on" })] }), _jsx(Text, { size: "xs", color: "muted", children: "Displayed in the notification bell and inbox. Cannot be disabled." })] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-email-notifications", children: _jsx(Toggle, { label: "Email notifications", checked: notifEmailEnabled, onChange: setNotifEmailEnabled }) }), notifEmailEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send email", options: PRIORITY_OPTIONS, value: notifEmailMinPriority, onValueChange: setNotifEmailMinPriority }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Resend API (for transactional email)" }), _jsx(MaskedInput, { label: "Resend API Key", value: resendApiKey, onChange: setResendApiKey, placeholder: "re_live_\u2026", helperText: "Get your key at resend.com \u2014 used for all transactional notifications." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "From email", value: notifFromEmail, onChange: (e) => setNotifFromEmail(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Input, { label: "From name", value: notifFromName, onChange: (e) => setNotifFromName(e.target.value), placeholder: "LetItRip" })] }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifEmailTypes, onChange: (values) => setNotifEmailTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-daily-digest", children: _jsx(Toggle, { label: "Daily status digest", checked: digestEnabled, onChange: setDigestEnabled }) }), digestEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Text, { size: "xs", color: "muted", children: "Sent every morning at 10:00 IST with the previous 24 hours of order activity. Its arrival doubles as the platform health check \u2014 if it stops landing, something needs a look." }), _jsx(Textarea, { label: "Recipients (To)", value: digestRecipients, onChange: (e) => setDigestRecipients(e.target.value), placeholder: "support@letitrip.in\nmohasin@letitrip.in", rows: 4, helperText: "One address per line." }), _jsx(Textarea, { label: "CC recipients", value: digestCcRecipients, onChange: (e) => setDigestCcRecipients(e.target.value), placeholder: "One address per line", rows: 3, helperText: "Optional \u2014 anyone else who should receive a copy." })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-whatsapp-notifications", children: _jsx(Toggle, { label: "WhatsApp notifications", checked: notifWhatsappEnabled, onChange: setNotifWhatsappEnabled }) }), notifWhatsappEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send WhatsApp", options: PRIORITY_OPTIONS, value: notifWhatsappMinPriority, onValueChange: setNotifWhatsappMinPriority }), _jsx(Toggle, { label: "Enable WhatsApp OTP (for login and verification)", checked: notifWhatsappOtpEnabled, onChange: setNotifWhatsappOtpEnabled }), _jsx(Text, { size: "xs", color: "muted", children: "WhatsApp credentials are configured in the WhatsApp tab (\u246C). OTP messages use the same phone number." }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifWhatsappTypes, onChange: (values) => setNotifWhatsappTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "SMS notifications", checked: notifSmsEnabled, onChange: setNotifSmsEnabled }), notifSmsEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send SMS", options: PRIORITY_OPTIONS, value: notifSmsMinPriority, onValueChange: setNotifSmsMinPriority }), _jsx(Text, { size: "xs", color: "muted", children: "SMS gateway credentials (e.g. Twilio, MSG91) can be configured in the Integrations tab once an SMS provider is connected." })] }))] })] }) }), _jsx(TabsContent, { value: "legal", children: _jsx(SettingsTabForm, { id: "legal", label: "Legal", children: [
|
|
747
|
+
]), children: "+ Add tier" })] }), _jsx(Input, { label: "Auto-extend window (minutes before end)", value: String(autoExtendWindow), onChange: (e) => setAutoExtendWindow(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Extend auction end time if a bid arrives within this window." }), _jsx(Input, { label: "Settlement grace period (hours)", value: String(settlementGrace), onChange: (e) => setSettlementGrace(parseInt(e.target.value) || 0), type: "number", min: 1, helperText: "Time winner has to pay before the auction is re-listed." })] }) }), _jsx(TabsContent, { value: "limits", children: _jsx(SettingsTabForm, { id: "limits", label: "Limits", children: _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Max products per store", value: String(maxProductsPerStore), onChange: (e) => setMaxProductsPerStore(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max custom fields per product", value: String(maxCustomFields), onChange: (e) => setMaxCustomFields(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Max custom sections per product", value: String(maxCustomSections), onChange: (e) => setMaxCustomSections(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Order cancellation window (hours)", value: String(orderCancelWindow), onChange: (e) => setOrderCancelWindow(parseInt(e.target.value) || 0), type: "number", min: 0 })] }) }) }), _jsx(TabsContent, { value: "whatsapp", children: _jsxs(SettingsTabForm, { id: "whatsapp", label: "Whatsapp", children: [_jsx(Text, { size: "xs", color: "muted", children: "Platform-level WhatsApp Business Cloud API credentials. Used for automated purchase announcements to admin numbers when orders are placed. Store owners configure their own credentials in Store \u2192 WhatsApp." }), _jsx(Input, { label: "Phone Number ID", value: waPhoneNumberId, onChange: (e) => setWaPhoneNumberId(e.target.value), placeholder: "987654321098765", helperText: "From Meta for Developers \u2192 App \u2192 WhatsApp \u2192 API Setup" }), _jsx(MaskedInput, { label: "Cloud API System User Token", value: waCloudApiToken, onChange: setWaCloudApiToken, placeholder: "EAAxxxxxxxx\u2026", helperText: "Long-lived system user access token with WhatsApp Business permissions" }), _jsx(Input, { label: "Admin Notify Numbers", value: waAdminNotifyNumbers, onChange: (e) => setWaAdminNotifyNumbers(e.target.value), placeholder: "919876543210,918765432109", helperText: "Comma-separated, digits-only, include country code. These receive a WhatsApp message when any order is placed." }), _jsx(Text, { size: "xs", color: "muted", children: "Approved Meta message templates for the buyer-facing WhatsApp order-updates addon (Fees tab). Meta rejects business-initiated free-text WhatsApp messages outside a 24h customer-service window \u2014 a real template name here is required for that notification type to actually deliver. Leave blank to skip WhatsApp for that type (in-app + email still fire)." }), _jsx(Input, { label: "Template language code", value: waTemplateLanguage, onChange: (e) => setWaTemplateLanguage(e.target.value), placeholder: "en", helperText: "BCP-47 code the templates below were approved in, e.g. en or en_US." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Order placed template name", value: waTemplateOrderPlaced, onChange: (e) => setWaTemplateOrderPlaced(e.target.value), placeholder: "order_placed_update" }), _jsx(Input, { label: "Order confirmed template name", value: waTemplateOrderConfirmed, onChange: (e) => setWaTemplateOrderConfirmed(e.target.value), placeholder: "order_confirmed_update" }), _jsx(Input, { label: "Order shipped template name", value: waTemplateOrderShipped, onChange: (e) => setWaTemplateOrderShipped(e.target.value), placeholder: "order_shipped_update" }), _jsx(Input, { label: "Order delivered template name", value: waTemplateOrderDelivered, onChange: (e) => setWaTemplateOrderDelivered(e.target.value), placeholder: "order_delivered_update" }), _jsx(Input, { label: "Order cancelled template name", value: waTemplateOrderCancelled, onChange: (e) => setWaTemplateOrderCancelled(e.target.value), placeholder: "order_cancelled_update" }), _jsx(Input, { label: "Refund initiated template name", value: waTemplateRefundInitiated, onChange: (e) => setWaTemplateRefundInitiated(e.target.value), placeholder: "refund_initiated_update" })] })] }) }), _jsx(TabsContent, { value: "notifications", children: _jsxs(SettingsTabForm, { id: "notifications", label: "Notifications", children: [_jsx(Text, { size: "xs", color: "muted", children: "In-app notifications are always on. Enable external channels below to let the platform fan out to email, WhatsApp, or SMS. Users can further restrict which types they receive." }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "In-app (notification bell)" }), _jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "semibold", rounded: "full", padding: "pill-xs", children: "Always on" })] }), _jsx(Text, { size: "xs", color: "muted", children: "Displayed in the notification bell and inbox. Cannot be disabled." })] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-email-notifications", children: _jsx(Toggle, { label: "Email notifications", checked: notifEmailEnabled, onChange: setNotifEmailEnabled }) }), notifEmailEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send email", options: PRIORITY_OPTIONS, value: notifEmailMinPriority, onValueChange: setNotifEmailMinPriority }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Resend API (for transactional email)" }), _jsx(MaskedInput, { label: "Resend API Key", value: resendApiKey, onChange: setResendApiKey, placeholder: "re_live_\u2026", helperText: "Get your key at resend.com \u2014 used for all transactional notifications." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "From email", value: notifFromEmail, onChange: (e) => setNotifFromEmail(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Input, { label: "From name", value: notifFromName, onChange: (e) => setNotifFromName(e.target.value), placeholder: "LetItRip" })] }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifEmailTypes, onChange: (values) => setNotifEmailTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-daily-digest", children: _jsx(Toggle, { label: "Daily status digest", checked: digestEnabled, onChange: setDigestEnabled }) }), digestEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Text, { size: "xs", color: "muted", children: "Sent every morning at 10:00 IST with the previous 24 hours of order activity. Its arrival doubles as the platform health check \u2014 if it stops landing, something needs a look." }), _jsx(Textarea, { label: "Recipients (To)", value: digestRecipients, onChange: (e) => setDigestRecipients(e.target.value), placeholder: "support@letitrip.in\nmohasin@letitrip.in", rows: 4, helperText: "One address per line." }), _jsx(Textarea, { label: "CC recipients", value: digestCcRecipients, onChange: (e) => setDigestCcRecipients(e.target.value), placeholder: "One address per line", rows: 3, helperText: "Optional \u2014 anyone else who should receive a copy." })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-whatsapp-notifications", children: _jsx(Toggle, { label: "WhatsApp notifications", checked: notifWhatsappEnabled, onChange: setNotifWhatsappEnabled }) }), notifWhatsappEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send WhatsApp", options: PRIORITY_OPTIONS, value: notifWhatsappMinPriority, onValueChange: setNotifWhatsappMinPriority }), _jsx(Toggle, { label: "Enable WhatsApp OTP (for login and verification)", checked: notifWhatsappOtpEnabled, onChange: setNotifWhatsappOtpEnabled }), _jsx(Text, { size: "xs", color: "muted", children: "WhatsApp credentials are configured in the WhatsApp tab (\u246C). OTP messages use the same phone number." }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifWhatsappTypes, onChange: (values) => setNotifWhatsappTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "SMS notifications", checked: notifSmsEnabled, onChange: setNotifSmsEnabled }), notifSmsEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send SMS", options: PRIORITY_OPTIONS, value: notifSmsMinPriority, onValueChange: setNotifSmsMinPriority }), _jsx(Text, { size: "xs", color: "muted", children: "SMS gateway credentials (e.g. Twilio, MSG91) can be configured in the Integrations tab once an SMS provider is connected." })] }))] })] }) }), _jsx(TabsContent, { value: "legal", children: _jsx(SettingsTabForm, { id: "legal", label: "Legal", children: [
|
|
752
748
|
["Terms of Service", termsHtml, setTermsHtml],
|
|
753
749
|
["Privacy Policy", privacyHtml, setPrivacyHtml],
|
|
754
750
|
["Refund Policy", refundHtml, setRefundHtml],
|
|
@@ -127,8 +127,6 @@ export { AdminCarouselEditorView } from "./AdminCarouselEditorView";
|
|
|
127
127
|
export type { AdminCarouselEditorViewProps } from "./AdminCarouselEditorView";
|
|
128
128
|
export { AdminDashboardView } from "./AdminDashboardView";
|
|
129
129
|
export type { AdminDashboardViewProps } from "./AdminDashboardView";
|
|
130
|
-
export { AdminSiteView } from "./AdminSiteView";
|
|
131
|
-
export type { AdminSiteViewProps } from "./AdminSiteView";
|
|
132
130
|
export { AdminSiteSettingsView } from "./AdminSiteSettingsView";
|
|
133
131
|
export type { AdminSiteSettingsViewProps } from "./AdminSiteSettingsView";
|
|
134
132
|
export { AdminAnalyticsView } from "./AdminAnalyticsView";
|
|
@@ -68,7 +68,6 @@ export { AdminAdEditorView } from "./AdminAdEditorView";
|
|
|
68
68
|
export { AdminCarouselView } from "./AdminCarouselView";
|
|
69
69
|
export { AdminCarouselEditorView } from "./AdminCarouselEditorView";
|
|
70
70
|
export { AdminDashboardView } from "./AdminDashboardView";
|
|
71
|
-
export { AdminSiteView } from "./AdminSiteView";
|
|
72
71
|
export { AdminSiteSettingsView } from "./AdminSiteSettingsView";
|
|
73
72
|
export { AdminAnalyticsView } from "./AdminAnalyticsView";
|
|
74
73
|
export { AdminStatCard } from "./analytics/AdminStatCard";
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* @tag layer:schema
|
|
23
23
|
* @tag pattern:none
|
|
24
24
|
* @tag access:isomorphic
|
|
25
|
-
* @tag consumers:AdminAddressEditorView,AdminStoreEditorView
|
|
25
|
+
* @tag consumers:AdminAddressEditorView,AdminStoreEditorView
|
|
26
26
|
* @tag sideEffects:none
|
|
27
27
|
*/
|
|
28
28
|
import { z } from "zod";
|
|
@@ -231,8 +231,6 @@ export interface SiteSettingsIntegrations {
|
|
|
231
231
|
/** Admin-tunable platform caps. Enforced server-side; not buyer-facing. */
|
|
232
232
|
export interface SiteSettingsPlatformLimits {
|
|
233
233
|
maxProductsPerStore?: number;
|
|
234
|
-
maxImagesPerProduct?: number;
|
|
235
|
-
maxVideoSizeMb?: number;
|
|
236
234
|
maxCustomFieldsPerProduct?: number;
|
|
237
235
|
maxCustomSectionsPerProduct?: number;
|
|
238
236
|
orderCancellationWindowHours?: number;
|