@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.
Files changed (104) hide show
  1. package/dist/_internal/server/features/checkout/actions.js +9 -1
  2. package/dist/_internal/server/features/checkout/expand-order-items.js +9 -0
  3. package/dist/_internal/server/features/media/contextGuards.d.ts +3 -3
  4. package/dist/_internal/server/features/media/contextGuards.js +19 -5
  5. package/dist/_internal/server/features/orders/actions.d.ts +9 -0
  6. package/dist/_internal/server/features/orders/actions.js +47 -4
  7. package/dist/_internal/server/features/orders/adapters.js +10 -0
  8. package/dist/_internal/server/features/refunds/actions.d.ts +9 -1
  9. package/dist/_internal/server/features/refunds/actions.js +36 -3
  10. package/dist/_internal/server/features/seo/sitemap.js +2 -2
  11. package/dist/_internal/server/features/site-settings/adapters.js +1 -1
  12. package/dist/_internal/server/functions/scheduled.d.ts +1 -2
  13. package/dist/_internal/server/functions/scheduled.js +8 -9
  14. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +15 -0
  15. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +15 -6
  16. package/dist/_internal/server/jobs/handlers/index.d.ts +0 -1
  17. package/dist/_internal/server/jobs/handlers/index.js +0 -1
  18. package/dist/_internal/shared/features/orders/refund-copy.d.ts +12 -0
  19. package/dist/_internal/shared/features/orders/refund-copy.js +12 -0
  20. package/dist/_internal/shared/features/orders/return-reasons.d.ts +59 -0
  21. package/dist/_internal/shared/features/orders/return-reasons.js +96 -0
  22. package/dist/_internal/shared/features/orders/schema.d.ts +30 -0
  23. package/dist/_internal/shared/features/orders/schema.js +20 -0
  24. package/dist/_internal/shared/features/products/schema.d.ts +12 -0
  25. package/dist/_internal/shared/features/products/schema.js +4 -1
  26. package/dist/_internal/shared/media/limits.d.ts +14 -0
  27. package/dist/_internal/shared/media/limits.js +29 -0
  28. package/dist/client.d.ts +9 -1
  29. package/dist/client.js +19 -1
  30. package/dist/features/admin/components/AdminProductEditorView.js +6 -1
  31. package/dist/features/admin/components/AdminSiteSettingsView.js +2 -6
  32. package/dist/features/admin/components/index.d.ts +0 -2
  33. package/dist/features/admin/components/index.js +0 -1
  34. package/dist/features/admin/schemas/admin-editor-forms.js +1 -1
  35. package/dist/features/admin/schemas/firestore.d.ts +0 -2
  36. package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
  37. package/dist/features/auth/schemas/firestore.d.ts +1 -0
  38. package/dist/features/contact/components/ContactForm.js +46 -31
  39. package/dist/features/media/upload/MediaUploadList.d.ts +16 -1
  40. package/dist/features/media/upload/MediaUploadList.js +58 -8
  41. package/dist/features/orders/components/RefundRequestView.d.ts +26 -10
  42. package/dist/features/orders/components/RefundRequestView.js +108 -24
  43. package/dist/features/orders/schemas/firestore.d.ts +42 -0
  44. package/dist/features/orders/types/index.d.ts +24 -0
  45. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +5 -3
  46. package/dist/features/products/components/ProductDetailPageView.js +14 -2
  47. package/dist/features/products/components/ProductFeatureBadges.d.ts +9 -1
  48. package/dist/features/products/components/ProductFeatureBadges.js +17 -1
  49. package/dist/features/products/components/ProductForm.js +46 -10
  50. package/dist/features/products/components/ProductGrid.js +10 -1
  51. package/dist/features/products/constants/final-sale.d.ts +40 -0
  52. package/dist/features/products/constants/final-sale.js +40 -0
  53. package/dist/features/products/index.d.ts +1 -0
  54. package/dist/features/products/index.js +1 -0
  55. package/dist/features/products/schemas/firestore.d.ts +19 -2
  56. package/dist/features/products/schemas/firestore.js +4 -0
  57. package/dist/features/products/types/index.d.ts +2 -0
  58. package/dist/features/seller/components/SellerProductShell.d.ts +7 -0
  59. package/dist/features/seller/components/SellerProductShell.js +82 -11
  60. package/dist/features/seller/index.d.ts +1 -0
  61. package/dist/features/seller/index.js +1 -0
  62. package/dist/features/seller/schemas/product-form.d.ts +304 -0
  63. package/dist/features/seller/schemas/product-form.js +132 -0
  64. package/dist/features/seller/utils/product-draft-mapping.d.ts +117 -0
  65. package/dist/features/seller/utils/product-draft-mapping.js +147 -0
  66. package/dist/features/shell/SectionForm.js +62 -7
  67. package/dist/features/shell/form-sections.js +4 -0
  68. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +16 -1
  69. package/dist/features/tester/repository/tester-checklist-item.repository.js +32 -6
  70. package/dist/features/tester/seed-data/events-tester-seed-data.js +77 -5
  71. package/dist/features/tester/seed-data/offers-tester-seed-data.js +23 -9
  72. package/dist/features/tester/seed-data/products-tester-seed-data.js +65 -49
  73. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +182 -0
  74. package/dist/features/tester/seed-data/tester-window.d.ts +25 -0
  75. package/dist/features/tester/seed-data/tester-window.js +66 -0
  76. package/dist/index.d.ts +6 -3
  77. package/dist/index.js +12 -3
  78. package/dist/next/routing/route-map.d.ts +2 -0
  79. package/dist/next/routing/route-map.js +1 -0
  80. package/dist/schemas/registry.d.ts +271 -0
  81. package/dist/schemas/registry.js +9 -0
  82. package/dist/seed/_helpers/final-sale-fixtures.d.ts +35 -0
  83. package/dist/seed/_helpers/final-sale-fixtures.js +61 -0
  84. package/dist/seed/products-art-seed-data.js +9 -1
  85. package/dist/seed/products-auctions-seed-data.js +2 -1
  86. package/dist/seed/products-classifieds-seed-data.js +2 -1
  87. package/dist/seed/products-digital-codes-seed-data.js +2 -1
  88. package/dist/seed/products-live-items-seed-data.js +2 -1
  89. package/dist/seed/products-preorders-seed-data.d.ts +2 -138
  90. package/dist/seed/products-preorders-seed-data.js +2 -1
  91. package/dist/seed/products-prize-draws-seed-data.js +2 -1
  92. package/dist/seed/products-standard-seed-data.js +2 -1
  93. package/dist/seed/products-stickers-seed-data.js +9 -1
  94. package/dist/seed/users-seed-data.js +107 -4
  95. package/dist/server.d.ts +6 -2
  96. package/dist/server.js +15 -2
  97. package/dist/styles.css +1 -1
  98. package/dist/tailwind-utilities.css +1 -1
  99. package/dist/tokens/motion.d.ts +12 -0
  100. package/dist/tokens/motion.js +12 -0
  101. package/dist/ui/components/CollapsibleSection.d.ts +18 -1
  102. package/dist/ui/components/CollapsibleSection.js +20 -3
  103. package/dist/ui/components/Motion.js +17 -1
  104. 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(20).default([]),
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.array(z.string()).max(5, "Up to 5 gallery images allowed").optional(),
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, maxImagesPerProduct, maxVideoSizeMb, maxCustomFieldsPerProduct: maxCustomFields, maxCustomSectionsPerProduct: maxCustomSections, orderCancellationWindowHours: orderCancelWindow },
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,AdminSiteView
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;