@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
@@ -28,6 +28,13 @@ export interface SellerProductDraft {
28
28
  insuranceCost?: number;
29
29
  gstRate?: 0 | 5 | 12 | 18 | 28;
30
30
  hsnCode?: string;
31
+ /**
32
+ * Absent means FINAL SALE — the form seeds it explicitly so the toggle has
33
+ * a value to render, but a draft that never touched the section still saves
34
+ * `undefined`, which the platform reads as final sale. Read via isFinalSale.
35
+ */
36
+ finalSale?: boolean;
37
+ returnPolicy?: string;
31
38
  status?: "draft" | "published";
32
39
  seoTitle?: string;
33
40
  seoDescription?: string;
@@ -12,6 +12,8 @@ import { QuickProductForm } from "./QuickProductForm";
12
12
  import { BarcodeField } from "./BarcodeField";
13
13
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
14
14
  import { DEFAULT_MIN_OFFER_PERCENT } from "../../../_internal/shared/features/offers/config";
15
+ import { PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "../../../_internal/shared/media/limits";
16
+ import { isFinalSale } from "../../products/constants/final-sale";
15
17
  import { normalizeError } from "../../../errors/normalize";
16
18
  import { toUserMessage } from "../../../errors/error-display-map";
17
19
  /**
@@ -46,8 +48,13 @@ const sellerProductSchema = z.object({
46
48
  category: z.string().optional(),
47
49
  brand: z.string().optional(),
48
50
  isActive: z.boolean().optional(),
49
- images: z.array(z.string()).max(5, "Up to 5 gallery images allowed").optional(),
51
+ images: z
52
+ .array(z.string())
53
+ .max(PRODUCT_MAX_IMAGES, `Up to ${PRODUCT_MAX_IMAGES} gallery images allowed`)
54
+ .optional(),
50
55
  video: productVideoSchema.optional(),
56
+ finalSale: z.boolean().optional(),
57
+ returnPolicy: z.string().max(1000).optional(),
51
58
  }).passthrough();
52
59
  const CONDITION_OPTIONS = [
53
60
  { value: "new", label: "New" },
@@ -76,26 +83,64 @@ function StepBasic({ values, onChange, renderCategorySelector, renderBrandSelect
76
83
  function StepMedia({ values, onChange, storeSlug = "store", }) {
77
84
  const { upload } = useMediaUpload();
78
85
  const idxRef = useRef(0);
79
- const galleryFields = (values.images ?? []).map((url) => ({
80
- url,
81
- type: "image",
82
- }));
86
+ /*
87
+ * One control for both media kinds.
88
+ *
89
+ * The document keeps `images: string[]` and a single `video` — unchanged —
90
+ * so this seeds from both and splits again on write. Collapsing the two
91
+ * into one stored `media[]` would be a Firestore migration touching cards,
92
+ * detail, cart, orders and search; the input surface is what needed fixing.
93
+ */
94
+ const galleryFields = [
95
+ ...(values.images ?? []).map((url) => ({ url, type: "image" })),
96
+ ...(values.video?.url
97
+ ? [{
98
+ url: values.video.url,
99
+ type: "video",
100
+ ...(values.video.thumbnailUrl ? { thumbnailUrl: values.video.thumbnailUrl } : {}),
101
+ }]
102
+ : []),
103
+ ];
104
+ /** Split a mixed gallery back into the document's two fields. */
105
+ const applyGallery = (fields) => {
106
+ const video = fields.find((f) => f.type === "video");
107
+ onChange({
108
+ images: fields.filter((f) => f.type === "image").map((f) => f.url),
109
+ video: video
110
+ ? {
111
+ ...(values.video ?? { type: "video", source: "upload" }),
112
+ ...video,
113
+ type: "video",
114
+ }
115
+ : undefined,
116
+ });
117
+ };
83
118
  return (_jsxs(Stack, { gap: "md", children: [_jsx(ImageUpload, { currentImage: values.mainImage, onUpload: (file) => upload(file, "products", true, {
84
119
  type: "product-image",
85
120
  index: 1,
86
121
  name: values.title ?? "product",
87
122
  store: storeSlug,
88
123
  category: values.category ?? "uncategorized",
89
- }), onChange: (url) => onChange({ mainImage: url }), label: "Main Image", helperText: "Recommended: 800\u00D7800px square (JPG, PNG, WebP \u2014 max 10 MB)", enableAdvancedCrop: true, cropAspectRatio: 1 }), _jsx(MediaUploadList, { label: "Gallery Images (up to 5)", value: galleryFields, onChange: (fields) => onChange({ images: fields.map((f) => f.url) }), onUpload: (file) => {
124
+ }), onChange: (url) => onChange({ mainImage: url }), label: "Main Image", helperText: "Recommended: 800\u00D7800px square (JPG, PNG, WebP \u2014 max 10 MB)", enableAdvancedCrop: true, cropAspectRatio: 1 }), _jsx(MediaUploadList, { label: `Gallery (up to ${PRODUCT_MAX_IMAGES} images + ${PRODUCT_MAX_VIDEOS} video)`, value: galleryFields, onChange: applyGallery, onUpload: (file) => {
125
+ /*
126
+ * The gallery occupies indices 2..N — index 1 is the main image, and
127
+ * `generateMediaFilename` bakes the index into the filename that the
128
+ * server's `indexGuard` then checks.
129
+ *
130
+ * This was `idxRef.current + 1` off a ref starting at 0, so the FIRST
131
+ * gallery image was index 2 and the fifth was index 6 — one past the
132
+ * server's ceiling of 5. The last slot of every product form returned
133
+ * a 400.
134
+ */
90
135
  idxRef.current += 1;
91
136
  return upload(file, "products", true, {
92
- type: "product-image",
93
- index: idxRef.current + 1,
137
+ type: file.type.startsWith("video/") ? "product-video" : "product-image",
138
+ index: Math.min(idxRef.current + 1, PRODUCT_IMAGE_INDEX_MAX),
94
139
  name: values.title ?? "product",
95
140
  store: storeSlug,
96
141
  category: values.category ?? "uncategorized",
97
142
  });
98
- }, accept: "image/*,video/*", maxItems: 5, maxSizeMB: 10, helperText: "Show multiple angles, grading details, or box contents." }), _jsx(MediaUploadField, { label: "Product Video (optional)", value: values.video?.url ?? "", onChange: (url) => onChange({
143
+ }, accept: "image/*,video/*", maxItems: PRODUCT_MAX_IMAGES + PRODUCT_MAX_VIDEOS, maxImages: PRODUCT_MAX_IMAGES, maxVideos: PRODUCT_MAX_VIDEOS, maxSizeMB: 50, helperText: "Show multiple angles, grading details, or box contents. A video can go here too." }), _jsx(MediaUploadField, { label: "Video options \u2014 YouTube, external URL, trim and poster", value: values.video?.url ?? "", onChange: (url) => onChange({
99
144
  video: url
100
145
  ? { ...(values.video ?? { type: "video", source: "upload" }), url }
101
146
  : undefined,
@@ -103,7 +148,7 @@ function StepMedia({ values, onChange, storeSlug = "store", }) {
103
148
  type: "product-video",
104
149
  name: values.title ?? "product",
105
150
  store: storeSlug,
106
- }), kind: "video", helperText: "MP4, WebM or QuickTime \u2014 max 50 MB" }), _jsx(FormField, { name: "externalVideoUrl", label: "3rd-party video URL (YouTube / Vimeo)", type: "text", value: values.externalVideoUrl ?? "", onChange: (v) => onChange({ externalVideoUrl: v }), placeholder: "https://www.youtube.com/watch?v=\u2026", hint: "External video links are queued for moderation and become visible to buyers after admin approval." }), _jsx(FormField, { name: "youtubeId", label: "YouTube Video ID (optional)", type: "text", value: values.youtubeId ?? "", onChange: (v) => onChange({ youtubeId: v }), placeholder: "e.g. dQw4w9WgXcQ", helpText: "Paste the 11-character ID from the YouTube URL" })] }));
151
+ }), kind: "auto", helperText: "The gallery above accepts a video file directly. Use this panel for a YouTube or external URL, or to trim and pick a poster frame." }), _jsx(FormField, { name: "externalVideoUrl", label: "3rd-party video URL (YouTube / Vimeo)", type: "text", value: values.externalVideoUrl ?? "", onChange: (v) => onChange({ externalVideoUrl: v }), placeholder: "https://www.youtube.com/watch?v=\u2026", hint: "External video links are queued for moderation and become visible to buyers after admin approval." }), _jsx(FormField, { name: "youtubeId", label: "YouTube Video ID (optional)", type: "text", value: values.youtubeId ?? "", onChange: (v) => onChange({ youtubeId: v }), placeholder: "e.g. dQw4w9WgXcQ", helpText: "Paste the 11-character ID from the YouTube URL" })] }));
107
152
  }
108
153
  // ── Step 3: Auction Settings ───────────────────────────────────────────────
109
154
  function StepAuctionSettings({ values, onChange, }) {
@@ -263,6 +308,25 @@ function StepShipping({ values, onChange, renderAddressSelector, }) {
263
308
  { value: "28", label: "28%" },
264
309
  ] }), _jsx(FormField, { name: "hsnCode", label: "HSN Code", value: values.hsnCode ?? "", onChange: (v) => onChange({ hsnCode: v }), placeholder: "e.g. 9503" })] }));
265
310
  }
311
+ // ── Step: Returns ─────────────────────────────────────────────────────────
312
+ /**
313
+ * Final sale + return policy.
314
+ *
315
+ * `returnPolicy` had no seller-facing editor at all before this section — the
316
+ * only place it could be authored was the admin field group, so a seller could
317
+ * never state their own terms.
318
+ *
319
+ * The toggle is phrased positively ("Accept returns") rather than as
320
+ * "Final sale", because a switch whose ON state means "fewer rights for the
321
+ * buyer" reads backwards and invites the seller to flip it by accident. The
322
+ * stored field is still `finalSale`, and OFF is its default.
323
+ */
324
+ function StepReturns({ values, onChange, }) {
325
+ const acceptsReturns = !isFinalSale(values);
326
+ return (_jsxs(Stack, { gap: "md", children: [_jsx(Toggle, { checked: acceptsReturns, onChange: (checked) => onChange({ finalSale: !checked }), label: "Accept change-of-mind returns" }), _jsx(Alert, { variant: acceptsReturns ? "info" : "warning", children: acceptsReturns
327
+ ? "Buyers can return this item within the platform return window for any reason, including simply changing their mind."
328
+ : "This is a FINAL SALE — the default. Buyers cannot return it for changing their mind. They can still claim if the item never arrived, arrived damaged, was the wrong item, was not as described, or was counterfeit." }), _jsx(FormField, { name: "returnPolicy", label: "Return policy (optional)", type: "textarea", value: values.returnPolicy ?? "", onChange: (v) => onChange({ returnPolicy: v }), placeholder: "Any extra detail buyers should know \u2014 condition on return, who pays return shipping, how long you take to refund.", hint: "Shown on the listing under Delivery & Returns. This is your own wording; it does not change what the platform allows above." })] }));
329
+ }
266
330
  // ── Step: Publish / SEO ───────────────────────────────────────────────────
267
331
  function StepPublish({ values, onChange, }) {
268
332
  return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "status", label: "Listing Status", type: "select", value: values.status ?? "draft", onChange: (v) => onChange({ status: v }), options: [
@@ -276,6 +340,7 @@ const EDIT_SECTIONS = [
276
340
  { id: "media", label: "Media" },
277
341
  { id: "pricing", label: "Pricing" },
278
342
  { id: "shipping", label: "Shipping" },
343
+ { id: "returns", label: "Returns" },
279
344
  { id: "publish", label: "Publish" },
280
345
  ];
281
346
  // ── Main SellerProductShell ───────────────────────────────────────────────
@@ -447,6 +512,12 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
447
512
  fields: ["shippingPaidBy", "pickupAddressId", "insurance", "insuranceCost"],
448
513
  render: ({ values, onChange }) => (_jsx(StepShipping, { values: values, onChange: onChange, renderAddressSelector: renderAddressSelector })),
449
514
  },
515
+ {
516
+ id: "returns",
517
+ label: "Returns",
518
+ fields: ["finalSale", "returnPolicy"],
519
+ render: ({ values, onChange }) => (_jsx(StepReturns, { values: values, onChange: onChange })),
520
+ },
450
521
  {
451
522
  id: "publish",
452
523
  label: "Publish",
@@ -505,5 +576,5 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
505
576
  ...EDIT_SECTIONS,
506
577
  ...(typeSpecificSection ? [{ id: typeSpecificSection.id, label: typeSpecificSection.label }] : []),
507
578
  ];
508
- return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, footerTopSlot: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(FormErrorSummary, {}) }), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), typeSpecificSection && (_jsxs(Section, { id: typeSpecificSection.id, children: [_jsx(Heading, { level: 3, className: "mb-4", children: typeSpecificSection.sectionHeading ?? typeSpecificSection.label }), typeSpecificSection.render({ values: draft, onChange: update, errors: {} })] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-secondary-text)]", size: "xs", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }) }));
579
+ return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, footerTopSlot: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(FormErrorSummary, {}) }), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), typeSpecificSection && (_jsxs(Section, { id: typeSpecificSection.id, children: [_jsx(Heading, { level: 3, className: "mb-4", children: typeSpecificSection.sectionHeading ?? typeSpecificSection.label }), typeSpecificSection.render({ values: draft, onChange: update, errors: {} })] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "returns", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Returns" }), _jsx(StepReturns, { values: draft, onChange: update })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-secondary-text)]", size: "xs", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }) }));
509
580
  }
@@ -8,3 +8,4 @@ export * from "./components";
8
8
  export * from "./schemas";
9
9
  export * from "./columns";
10
10
  export { manifest } from "./manifest";
11
+ export { draftToProductInput, productToDraft } from "./utils/product-draft-mapping";
@@ -8,3 +8,4 @@ export * from "./components";
8
8
  export * from "./schemas";
9
9
  export * from "./columns";
10
10
  export { manifest } from "./manifest";
11
+ export { draftToProductInput, productToDraft } from "./utils/product-draft-mapping";
@@ -0,0 +1,304 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * The seller listing form's DRAFT schema — the flat, form-shaped view of a
4
+ * product, registered as `SCHEMAS.forms.product`.
5
+ *
6
+ * ## Why this exists as a separate file
7
+ *
8
+ * It is registered so `roundtrip-diff` can check it. That tool reads
9
+ * `SCHEMAS.forms`, parses real documents through each registered schema, and
10
+ * reports every key the schema would DELETE — and its own header names
11
+ * `productBaseSchema` eating the `classified*` / `liveItem*` fields as the
12
+ * reason it was written.
13
+ *
14
+ * `product` was never registered. So the one tool built to catch exactly this
15
+ * bug could not see the entity the bug was about, and the registry's own
16
+ * docstring said why: *"an entity absent from it has no such protection,
17
+ * which is the whole reason to register early."*
18
+ *
19
+ * ## Draft-shaped, deliberately
20
+ *
21
+ * This models what the user TYPES — flat `classifiedCity`, `liveSpecies`,
22
+ * `printSize` — not what is stored. `draftToProductInput` translates between
23
+ * the two. `roundtrip-diff` lists `product` in its `DRAFT_SHAPED` set for the
24
+ * same reason `blog` and `event` are there: real documents legitimately fail
25
+ * to parse against a draft, and only the DROPPED-KEY half of the check is
26
+ * meaningful. That half is the half that matters.
27
+ *
28
+ * Kept permissive (`.passthrough()`, almost everything optional): a draft is
29
+ * saved continuously while half-filled, so this must accept an in-progress
30
+ * listing. The strict rules live in `productCreateSchema` at the write
31
+ * boundary, which is where refusing a bad listing is useful.
32
+ */
33
+ export declare const productDraftSchema: z.ZodObject<{
34
+ title: z.ZodOptional<z.ZodString>;
35
+ slug: z.ZodOptional<z.ZodString>;
36
+ description: z.ZodOptional<z.ZodString>;
37
+ category: z.ZodOptional<z.ZodString>;
38
+ brand: z.ZodOptional<z.ZodString>;
39
+ condition: z.ZodOptional<z.ZodString>;
40
+ tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
41
+ mainImage: z.ZodOptional<z.ZodString>;
42
+ images: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
43
+ video: z.ZodOptional<z.ZodObject<{
44
+ url: z.ZodString;
45
+ type: z.ZodOptional<z.ZodLiteral<"video">>;
46
+ thumbnailUrl: z.ZodOptional<z.ZodString>;
47
+ source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
48
+ youtubeId: z.ZodOptional<z.ZodString>;
49
+ duration: z.ZodOptional<z.ZodNumber>;
50
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
51
+ url: z.ZodString;
52
+ type: z.ZodOptional<z.ZodLiteral<"video">>;
53
+ thumbnailUrl: z.ZodOptional<z.ZodString>;
54
+ source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
55
+ youtubeId: z.ZodOptional<z.ZodString>;
56
+ duration: z.ZodOptional<z.ZodNumber>;
57
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
58
+ url: z.ZodString;
59
+ type: z.ZodOptional<z.ZodLiteral<"video">>;
60
+ thumbnailUrl: z.ZodOptional<z.ZodString>;
61
+ source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
62
+ youtubeId: z.ZodOptional<z.ZodString>;
63
+ duration: z.ZodOptional<z.ZodNumber>;
64
+ }, z.ZodTypeAny, "passthrough">>>;
65
+ youtubeId: z.ZodOptional<z.ZodString>;
66
+ externalVideoUrl: z.ZodOptional<z.ZodString>;
67
+ price: z.ZodOptional<z.ZodNumber>;
68
+ compareAtPrice: z.ZodOptional<z.ZodNumber>;
69
+ stockQuantity: z.ZodOptional<z.ZodNumber>;
70
+ featured: z.ZodOptional<z.ZodBoolean>;
71
+ isPromoted: z.ZodOptional<z.ZodBoolean>;
72
+ isNew: z.ZodOptional<z.ZodBoolean>;
73
+ isOnSale: z.ZodOptional<z.ZodBoolean>;
74
+ allowOffers: z.ZodOptional<z.ZodBoolean>;
75
+ minOfferPercent: z.ZodOptional<z.ZodNumber>;
76
+ shippingPaidBy: z.ZodOptional<z.ZodEnum<["buyer", "seller"]>>;
77
+ pickupAddressId: z.ZodOptional<z.ZodString>;
78
+ insurance: z.ZodOptional<z.ZodBoolean>;
79
+ insuranceCost: z.ZodOptional<z.ZodNumber>;
80
+ gstRate: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<0>, z.ZodLiteral<5>, z.ZodLiteral<12>, z.ZodLiteral<18>, z.ZodLiteral<28>]>>;
81
+ hsnCode: z.ZodOptional<z.ZodString>;
82
+ finalSale: z.ZodOptional<z.ZodBoolean>;
83
+ returnPolicy: z.ZodOptional<z.ZodString>;
84
+ status: z.ZodOptional<z.ZodEnum<["draft", "published"]>>;
85
+ seoTitle: z.ZodOptional<z.ZodString>;
86
+ seoDescription: z.ZodOptional<z.ZodString>;
87
+ barcodeId: z.ZodOptional<z.ZodString>;
88
+ startingBid: z.ZodOptional<z.ZodNumber>;
89
+ reservePrice: z.ZodOptional<z.ZodNumber>;
90
+ buyNowPrice: z.ZodOptional<z.ZodNumber>;
91
+ minBidIncrement: z.ZodOptional<z.ZodNumber>;
92
+ auctionEndDate: z.ZodOptional<z.ZodString>;
93
+ auctionShippingPaidBy: z.ZodOptional<z.ZodEnum<["winner", "seller"]>>;
94
+ autoExtendable: z.ZodOptional<z.ZodBoolean>;
95
+ auctionExtensionMinutes: z.ZodOptional<z.ZodNumber>;
96
+ preOrderDeliveryDate: z.ZodOptional<z.ZodString>;
97
+ preOrderDepositPercent: z.ZodOptional<z.ZodNumber>;
98
+ preOrderMaxQuantity: z.ZodOptional<z.ZodNumber>;
99
+ preOrderProductionStatus: z.ZodOptional<z.ZodEnum<["upcoming", "in_production", "ready_to_ship"]>>;
100
+ preOrderCancellable: z.ZodOptional<z.ZodBoolean>;
101
+ classifiedCity: z.ZodOptional<z.ZodString>;
102
+ classifiedLocality: z.ZodOptional<z.ZodString>;
103
+ classifiedPincode: z.ZodOptional<z.ZodString>;
104
+ classifiedContactMethod: z.ZodOptional<z.ZodEnum<["chat", "phone", "both"]>>;
105
+ classifiedAcceptsShipping: z.ZodOptional<z.ZodBoolean>;
106
+ classifiedNegotiable: z.ZodOptional<z.ZodBoolean>;
107
+ digitalCodeDelivery: z.ZodOptional<z.ZodEnum<["auto-claim", "manual-email"]>>;
108
+ digitalCodePoolSize: z.ZodOptional<z.ZodNumber>;
109
+ digitalCodeRedemptionInstructions: z.ZodOptional<z.ZodString>;
110
+ digitalCodeExpiresAt: z.ZodOptional<z.ZodString>;
111
+ liveSpecies: z.ZodOptional<z.ZodString>;
112
+ liveAgeMonths: z.ZodOptional<z.ZodNumber>;
113
+ liveSex: z.ZodOptional<z.ZodEnum<["male", "female", "unknown", "n/a"]>>;
114
+ liveCareInfo: z.ZodOptional<z.ZodString>;
115
+ liveTransportMethod: z.ZodOptional<z.ZodEnum<["courier", "in-person", "specialist"]>>;
116
+ liveHandlingFee: z.ZodOptional<z.ZodNumber>;
117
+ liveJurisdictions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
118
+ liveCites: z.ZodOptional<z.ZodBoolean>;
119
+ printSize: z.ZodOptional<z.ZodString>;
120
+ printMaterial: z.ZodOptional<z.ZodString>;
121
+ printFinish: z.ZodOptional<z.ZodString>;
122
+ printEditionSize: z.ZodOptional<z.ZodNumber>;
123
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
124
+ title: z.ZodOptional<z.ZodString>;
125
+ slug: z.ZodOptional<z.ZodString>;
126
+ description: z.ZodOptional<z.ZodString>;
127
+ category: z.ZodOptional<z.ZodString>;
128
+ brand: z.ZodOptional<z.ZodString>;
129
+ condition: z.ZodOptional<z.ZodString>;
130
+ tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
131
+ mainImage: z.ZodOptional<z.ZodString>;
132
+ images: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
133
+ video: z.ZodOptional<z.ZodObject<{
134
+ url: z.ZodString;
135
+ type: z.ZodOptional<z.ZodLiteral<"video">>;
136
+ thumbnailUrl: z.ZodOptional<z.ZodString>;
137
+ source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
138
+ youtubeId: z.ZodOptional<z.ZodString>;
139
+ duration: z.ZodOptional<z.ZodNumber>;
140
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
141
+ url: z.ZodString;
142
+ type: z.ZodOptional<z.ZodLiteral<"video">>;
143
+ thumbnailUrl: z.ZodOptional<z.ZodString>;
144
+ source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
145
+ youtubeId: z.ZodOptional<z.ZodString>;
146
+ duration: z.ZodOptional<z.ZodNumber>;
147
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
148
+ url: z.ZodString;
149
+ type: z.ZodOptional<z.ZodLiteral<"video">>;
150
+ thumbnailUrl: z.ZodOptional<z.ZodString>;
151
+ source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
152
+ youtubeId: z.ZodOptional<z.ZodString>;
153
+ duration: z.ZodOptional<z.ZodNumber>;
154
+ }, z.ZodTypeAny, "passthrough">>>;
155
+ youtubeId: z.ZodOptional<z.ZodString>;
156
+ externalVideoUrl: z.ZodOptional<z.ZodString>;
157
+ price: z.ZodOptional<z.ZodNumber>;
158
+ compareAtPrice: z.ZodOptional<z.ZodNumber>;
159
+ stockQuantity: z.ZodOptional<z.ZodNumber>;
160
+ featured: z.ZodOptional<z.ZodBoolean>;
161
+ isPromoted: z.ZodOptional<z.ZodBoolean>;
162
+ isNew: z.ZodOptional<z.ZodBoolean>;
163
+ isOnSale: z.ZodOptional<z.ZodBoolean>;
164
+ allowOffers: z.ZodOptional<z.ZodBoolean>;
165
+ minOfferPercent: z.ZodOptional<z.ZodNumber>;
166
+ shippingPaidBy: z.ZodOptional<z.ZodEnum<["buyer", "seller"]>>;
167
+ pickupAddressId: z.ZodOptional<z.ZodString>;
168
+ insurance: z.ZodOptional<z.ZodBoolean>;
169
+ insuranceCost: z.ZodOptional<z.ZodNumber>;
170
+ gstRate: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<0>, z.ZodLiteral<5>, z.ZodLiteral<12>, z.ZodLiteral<18>, z.ZodLiteral<28>]>>;
171
+ hsnCode: z.ZodOptional<z.ZodString>;
172
+ finalSale: z.ZodOptional<z.ZodBoolean>;
173
+ returnPolicy: z.ZodOptional<z.ZodString>;
174
+ status: z.ZodOptional<z.ZodEnum<["draft", "published"]>>;
175
+ seoTitle: z.ZodOptional<z.ZodString>;
176
+ seoDescription: z.ZodOptional<z.ZodString>;
177
+ barcodeId: z.ZodOptional<z.ZodString>;
178
+ startingBid: z.ZodOptional<z.ZodNumber>;
179
+ reservePrice: z.ZodOptional<z.ZodNumber>;
180
+ buyNowPrice: z.ZodOptional<z.ZodNumber>;
181
+ minBidIncrement: z.ZodOptional<z.ZodNumber>;
182
+ auctionEndDate: z.ZodOptional<z.ZodString>;
183
+ auctionShippingPaidBy: z.ZodOptional<z.ZodEnum<["winner", "seller"]>>;
184
+ autoExtendable: z.ZodOptional<z.ZodBoolean>;
185
+ auctionExtensionMinutes: z.ZodOptional<z.ZodNumber>;
186
+ preOrderDeliveryDate: z.ZodOptional<z.ZodString>;
187
+ preOrderDepositPercent: z.ZodOptional<z.ZodNumber>;
188
+ preOrderMaxQuantity: z.ZodOptional<z.ZodNumber>;
189
+ preOrderProductionStatus: z.ZodOptional<z.ZodEnum<["upcoming", "in_production", "ready_to_ship"]>>;
190
+ preOrderCancellable: z.ZodOptional<z.ZodBoolean>;
191
+ classifiedCity: z.ZodOptional<z.ZodString>;
192
+ classifiedLocality: z.ZodOptional<z.ZodString>;
193
+ classifiedPincode: z.ZodOptional<z.ZodString>;
194
+ classifiedContactMethod: z.ZodOptional<z.ZodEnum<["chat", "phone", "both"]>>;
195
+ classifiedAcceptsShipping: z.ZodOptional<z.ZodBoolean>;
196
+ classifiedNegotiable: z.ZodOptional<z.ZodBoolean>;
197
+ digitalCodeDelivery: z.ZodOptional<z.ZodEnum<["auto-claim", "manual-email"]>>;
198
+ digitalCodePoolSize: z.ZodOptional<z.ZodNumber>;
199
+ digitalCodeRedemptionInstructions: z.ZodOptional<z.ZodString>;
200
+ digitalCodeExpiresAt: z.ZodOptional<z.ZodString>;
201
+ liveSpecies: z.ZodOptional<z.ZodString>;
202
+ liveAgeMonths: z.ZodOptional<z.ZodNumber>;
203
+ liveSex: z.ZodOptional<z.ZodEnum<["male", "female", "unknown", "n/a"]>>;
204
+ liveCareInfo: z.ZodOptional<z.ZodString>;
205
+ liveTransportMethod: z.ZodOptional<z.ZodEnum<["courier", "in-person", "specialist"]>>;
206
+ liveHandlingFee: z.ZodOptional<z.ZodNumber>;
207
+ liveJurisdictions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
208
+ liveCites: z.ZodOptional<z.ZodBoolean>;
209
+ printSize: z.ZodOptional<z.ZodString>;
210
+ printMaterial: z.ZodOptional<z.ZodString>;
211
+ printFinish: z.ZodOptional<z.ZodString>;
212
+ printEditionSize: z.ZodOptional<z.ZodNumber>;
213
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
214
+ title: z.ZodOptional<z.ZodString>;
215
+ slug: z.ZodOptional<z.ZodString>;
216
+ description: z.ZodOptional<z.ZodString>;
217
+ category: z.ZodOptional<z.ZodString>;
218
+ brand: z.ZodOptional<z.ZodString>;
219
+ condition: z.ZodOptional<z.ZodString>;
220
+ tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
221
+ mainImage: z.ZodOptional<z.ZodString>;
222
+ images: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
223
+ video: z.ZodOptional<z.ZodObject<{
224
+ url: z.ZodString;
225
+ type: z.ZodOptional<z.ZodLiteral<"video">>;
226
+ thumbnailUrl: z.ZodOptional<z.ZodString>;
227
+ source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
228
+ youtubeId: z.ZodOptional<z.ZodString>;
229
+ duration: z.ZodOptional<z.ZodNumber>;
230
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
231
+ url: z.ZodString;
232
+ type: z.ZodOptional<z.ZodLiteral<"video">>;
233
+ thumbnailUrl: z.ZodOptional<z.ZodString>;
234
+ source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
235
+ youtubeId: z.ZodOptional<z.ZodString>;
236
+ duration: z.ZodOptional<z.ZodNumber>;
237
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
238
+ url: z.ZodString;
239
+ type: z.ZodOptional<z.ZodLiteral<"video">>;
240
+ thumbnailUrl: z.ZodOptional<z.ZodString>;
241
+ source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
242
+ youtubeId: z.ZodOptional<z.ZodString>;
243
+ duration: z.ZodOptional<z.ZodNumber>;
244
+ }, z.ZodTypeAny, "passthrough">>>;
245
+ youtubeId: z.ZodOptional<z.ZodString>;
246
+ externalVideoUrl: z.ZodOptional<z.ZodString>;
247
+ price: z.ZodOptional<z.ZodNumber>;
248
+ compareAtPrice: z.ZodOptional<z.ZodNumber>;
249
+ stockQuantity: z.ZodOptional<z.ZodNumber>;
250
+ featured: z.ZodOptional<z.ZodBoolean>;
251
+ isPromoted: z.ZodOptional<z.ZodBoolean>;
252
+ isNew: z.ZodOptional<z.ZodBoolean>;
253
+ isOnSale: z.ZodOptional<z.ZodBoolean>;
254
+ allowOffers: z.ZodOptional<z.ZodBoolean>;
255
+ minOfferPercent: z.ZodOptional<z.ZodNumber>;
256
+ shippingPaidBy: z.ZodOptional<z.ZodEnum<["buyer", "seller"]>>;
257
+ pickupAddressId: z.ZodOptional<z.ZodString>;
258
+ insurance: z.ZodOptional<z.ZodBoolean>;
259
+ insuranceCost: z.ZodOptional<z.ZodNumber>;
260
+ gstRate: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<0>, z.ZodLiteral<5>, z.ZodLiteral<12>, z.ZodLiteral<18>, z.ZodLiteral<28>]>>;
261
+ hsnCode: z.ZodOptional<z.ZodString>;
262
+ finalSale: z.ZodOptional<z.ZodBoolean>;
263
+ returnPolicy: z.ZodOptional<z.ZodString>;
264
+ status: z.ZodOptional<z.ZodEnum<["draft", "published"]>>;
265
+ seoTitle: z.ZodOptional<z.ZodString>;
266
+ seoDescription: z.ZodOptional<z.ZodString>;
267
+ barcodeId: z.ZodOptional<z.ZodString>;
268
+ startingBid: z.ZodOptional<z.ZodNumber>;
269
+ reservePrice: z.ZodOptional<z.ZodNumber>;
270
+ buyNowPrice: z.ZodOptional<z.ZodNumber>;
271
+ minBidIncrement: z.ZodOptional<z.ZodNumber>;
272
+ auctionEndDate: z.ZodOptional<z.ZodString>;
273
+ auctionShippingPaidBy: z.ZodOptional<z.ZodEnum<["winner", "seller"]>>;
274
+ autoExtendable: z.ZodOptional<z.ZodBoolean>;
275
+ auctionExtensionMinutes: z.ZodOptional<z.ZodNumber>;
276
+ preOrderDeliveryDate: z.ZodOptional<z.ZodString>;
277
+ preOrderDepositPercent: z.ZodOptional<z.ZodNumber>;
278
+ preOrderMaxQuantity: z.ZodOptional<z.ZodNumber>;
279
+ preOrderProductionStatus: z.ZodOptional<z.ZodEnum<["upcoming", "in_production", "ready_to_ship"]>>;
280
+ preOrderCancellable: z.ZodOptional<z.ZodBoolean>;
281
+ classifiedCity: z.ZodOptional<z.ZodString>;
282
+ classifiedLocality: z.ZodOptional<z.ZodString>;
283
+ classifiedPincode: z.ZodOptional<z.ZodString>;
284
+ classifiedContactMethod: z.ZodOptional<z.ZodEnum<["chat", "phone", "both"]>>;
285
+ classifiedAcceptsShipping: z.ZodOptional<z.ZodBoolean>;
286
+ classifiedNegotiable: z.ZodOptional<z.ZodBoolean>;
287
+ digitalCodeDelivery: z.ZodOptional<z.ZodEnum<["auto-claim", "manual-email"]>>;
288
+ digitalCodePoolSize: z.ZodOptional<z.ZodNumber>;
289
+ digitalCodeRedemptionInstructions: z.ZodOptional<z.ZodString>;
290
+ digitalCodeExpiresAt: z.ZodOptional<z.ZodString>;
291
+ liveSpecies: z.ZodOptional<z.ZodString>;
292
+ liveAgeMonths: z.ZodOptional<z.ZodNumber>;
293
+ liveSex: z.ZodOptional<z.ZodEnum<["male", "female", "unknown", "n/a"]>>;
294
+ liveCareInfo: z.ZodOptional<z.ZodString>;
295
+ liveTransportMethod: z.ZodOptional<z.ZodEnum<["courier", "in-person", "specialist"]>>;
296
+ liveHandlingFee: z.ZodOptional<z.ZodNumber>;
297
+ liveJurisdictions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
298
+ liveCites: z.ZodOptional<z.ZodBoolean>;
299
+ printSize: z.ZodOptional<z.ZodString>;
300
+ printMaterial: z.ZodOptional<z.ZodString>;
301
+ printFinish: z.ZodOptional<z.ZodString>;
302
+ printEditionSize: z.ZodOptional<z.ZodNumber>;
303
+ }, z.ZodTypeAny, "passthrough">>;
304
+ export type ProductDraftInput = z.infer<typeof productDraftSchema>;
@@ -0,0 +1,132 @@
1
+ import { z } from "zod";
2
+ import { PRODUCT_MAX_IMAGES } from "../../../_internal/shared/media/limits";
3
+ /**
4
+ * The seller listing form's DRAFT schema — the flat, form-shaped view of a
5
+ * product, registered as `SCHEMAS.forms.product`.
6
+ *
7
+ * ## Why this exists as a separate file
8
+ *
9
+ * It is registered so `roundtrip-diff` can check it. That tool reads
10
+ * `SCHEMAS.forms`, parses real documents through each registered schema, and
11
+ * reports every key the schema would DELETE — and its own header names
12
+ * `productBaseSchema` eating the `classified*` / `liveItem*` fields as the
13
+ * reason it was written.
14
+ *
15
+ * `product` was never registered. So the one tool built to catch exactly this
16
+ * bug could not see the entity the bug was about, and the registry's own
17
+ * docstring said why: *"an entity absent from it has no such protection,
18
+ * which is the whole reason to register early."*
19
+ *
20
+ * ## Draft-shaped, deliberately
21
+ *
22
+ * This models what the user TYPES — flat `classifiedCity`, `liveSpecies`,
23
+ * `printSize` — not what is stored. `draftToProductInput` translates between
24
+ * the two. `roundtrip-diff` lists `product` in its `DRAFT_SHAPED` set for the
25
+ * same reason `blog` and `event` are there: real documents legitimately fail
26
+ * to parse against a draft, and only the DROPPED-KEY half of the check is
27
+ * meaningful. That half is the half that matters.
28
+ *
29
+ * Kept permissive (`.passthrough()`, almost everything optional): a draft is
30
+ * saved continuously while half-filled, so this must accept an in-progress
31
+ * listing. The strict rules live in `productCreateSchema` at the write
32
+ * boundary, which is where refusing a bad listing is useful.
33
+ */
34
+ export const productDraftSchema = z
35
+ .object({
36
+ title: z.string().max(200).optional(),
37
+ slug: z.string().optional(),
38
+ description: z.string().max(10000).optional(),
39
+ category: z.string().optional(),
40
+ brand: z.string().optional(),
41
+ condition: z.string().optional(),
42
+ tags: z.array(z.string()).optional(),
43
+ // Media
44
+ mainImage: z.string().optional(),
45
+ images: z.array(z.string()).max(PRODUCT_MAX_IMAGES).optional(),
46
+ /*
47
+ * Spelled out rather than `z.record(z.unknown())`: a draft's video can
48
+ * come from an upload, a YouTube id or an external URL, and each source
49
+ * fills a different subset. Naming the fields is what lets this schema
50
+ * report a video the form would fail to carry.
51
+ */
52
+ video: z
53
+ .object({
54
+ url: z.string(),
55
+ type: z.literal("video").optional(),
56
+ thumbnailUrl: z.string().optional(),
57
+ source: z.enum(["upload", "youtube", "external"]).optional(),
58
+ youtubeId: z.string().optional(),
59
+ duration: z.number().optional(),
60
+ })
61
+ .passthrough()
62
+ .optional(),
63
+ youtubeId: z.string().optional(),
64
+ externalVideoUrl: z.string().optional(),
65
+ // Pricing
66
+ price: z.number().optional(),
67
+ compareAtPrice: z.number().optional(),
68
+ stockQuantity: z.number().optional(),
69
+ featured: z.boolean().optional(),
70
+ isPromoted: z.boolean().optional(),
71
+ isNew: z.boolean().optional(),
72
+ isOnSale: z.boolean().optional(),
73
+ allowOffers: z.boolean().optional(),
74
+ minOfferPercent: z.number().optional(),
75
+ // Shipping
76
+ shippingPaidBy: z.enum(["buyer", "seller"]).optional(),
77
+ pickupAddressId: z.string().optional(),
78
+ insurance: z.boolean().optional(),
79
+ insuranceCost: z.number().optional(),
80
+ gstRate: z.union([z.literal(0), z.literal(5), z.literal(12), z.literal(18), z.literal(28)]).optional(),
81
+ hsnCode: z.string().optional(),
82
+ // Returns
83
+ finalSale: z.boolean().optional(),
84
+ returnPolicy: z.string().max(1000).optional(),
85
+ // Publish
86
+ status: z.enum(["draft", "published"]).optional(),
87
+ seoTitle: z.string().optional(),
88
+ seoDescription: z.string().optional(),
89
+ barcodeId: z.string().optional(),
90
+ // Auction
91
+ startingBid: z.number().optional(),
92
+ reservePrice: z.number().optional(),
93
+ buyNowPrice: z.number().optional(),
94
+ minBidIncrement: z.number().optional(),
95
+ auctionEndDate: z.string().optional(),
96
+ auctionShippingPaidBy: z.enum(["winner", "seller"]).optional(),
97
+ autoExtendable: z.boolean().optional(),
98
+ auctionExtensionMinutes: z.number().optional(),
99
+ // Pre-order
100
+ preOrderDeliveryDate: z.string().optional(),
101
+ preOrderDepositPercent: z.number().optional(),
102
+ preOrderMaxQuantity: z.number().optional(),
103
+ preOrderProductionStatus: z.enum(["upcoming", "in_production", "ready_to_ship"]).optional(),
104
+ preOrderCancellable: z.boolean().optional(),
105
+ // Classified — FLAT here, nested on the document.
106
+ classifiedCity: z.string().optional(),
107
+ classifiedLocality: z.string().optional(),
108
+ classifiedPincode: z.string().optional(),
109
+ classifiedContactMethod: z.enum(["chat", "phone", "both"]).optional(),
110
+ classifiedAcceptsShipping: z.boolean().optional(),
111
+ classifiedNegotiable: z.boolean().optional(),
112
+ // Digital code — FLAT here, nested on the document.
113
+ digitalCodeDelivery: z.enum(["auto-claim", "manual-email"]).optional(),
114
+ digitalCodePoolSize: z.number().optional(),
115
+ digitalCodeRedemptionInstructions: z.string().optional(),
116
+ digitalCodeExpiresAt: z.string().optional(),
117
+ // Live item — FLAT here, nested on the document.
118
+ liveSpecies: z.string().optional(),
119
+ liveAgeMonths: z.number().optional(),
120
+ liveSex: z.enum(["male", "female", "unknown", "n/a"]).optional(),
121
+ liveCareInfo: z.string().optional(),
122
+ liveTransportMethod: z.enum(["courier", "in-person", "specialist"]).optional(),
123
+ liveHandlingFee: z.number().optional(),
124
+ liveJurisdictions: z.array(z.string()).optional(),
125
+ liveCites: z.boolean().optional(),
126
+ // Print meta (art + stickers) — FLAT here, nested on the document.
127
+ printSize: z.string().optional(),
128
+ printMaterial: z.string().optional(),
129
+ printFinish: z.string().optional(),
130
+ printEditionSize: z.number().optional(),
131
+ })
132
+ .passthrough();