@mohasinac/appkit 4.33.1 → 4.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/checkout/actions.js +9 -1
- package/dist/_internal/server/features/checkout/expand-order-items.js +9 -0
- package/dist/_internal/server/features/media/contextGuards.d.ts +3 -3
- package/dist/_internal/server/features/media/contextGuards.js +19 -5
- package/dist/_internal/server/features/orders/actions.d.ts +9 -0
- package/dist/_internal/server/features/orders/actions.js +47 -4
- package/dist/_internal/server/features/orders/adapters.js +10 -0
- package/dist/_internal/server/features/refunds/actions.d.ts +9 -1
- package/dist/_internal/server/features/refunds/actions.js +36 -3
- package/dist/_internal/server/features/seo/sitemap.js +2 -2
- package/dist/_internal/server/features/site-settings/adapters.js +1 -1
- package/dist/_internal/server/functions/scheduled.d.ts +1 -2
- package/dist/_internal/server/functions/scheduled.js +8 -9
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +15 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +15 -6
- package/dist/_internal/server/jobs/handlers/index.d.ts +0 -1
- package/dist/_internal/server/jobs/handlers/index.js +0 -1
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +12 -0
- package/dist/_internal/shared/features/orders/refund-copy.js +12 -0
- package/dist/_internal/shared/features/orders/return-reasons.d.ts +59 -0
- package/dist/_internal/shared/features/orders/return-reasons.js +96 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +30 -0
- package/dist/_internal/shared/features/orders/schema.js +20 -0
- package/dist/_internal/shared/features/products/schema.d.ts +12 -0
- package/dist/_internal/shared/features/products/schema.js +4 -1
- package/dist/_internal/shared/media/limits.d.ts +14 -0
- package/dist/_internal/shared/media/limits.js +29 -0
- package/dist/client.d.ts +9 -1
- package/dist/client.js +19 -1
- package/dist/features/admin/components/AdminProductEditorView.js +6 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +2 -6
- package/dist/features/admin/components/index.d.ts +0 -2
- package/dist/features/admin/components/index.js +0 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +1 -1
- package/dist/features/admin/schemas/firestore.d.ts +0 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
- package/dist/features/auth/schemas/firestore.d.ts +1 -0
- package/dist/features/contact/components/ContactForm.js +46 -31
- package/dist/features/media/upload/MediaUploadList.d.ts +16 -1
- package/dist/features/media/upload/MediaUploadList.js +58 -8
- package/dist/features/orders/components/RefundRequestView.d.ts +26 -10
- package/dist/features/orders/components/RefundRequestView.js +108 -24
- package/dist/features/orders/schemas/firestore.d.ts +42 -0
- package/dist/features/orders/types/index.d.ts +24 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +5 -3
- package/dist/features/products/components/ProductDetailPageView.js +14 -2
- package/dist/features/products/components/ProductFeatureBadges.d.ts +9 -1
- package/dist/features/products/components/ProductFeatureBadges.js +17 -1
- package/dist/features/products/components/ProductForm.js +46 -10
- package/dist/features/products/components/ProductGrid.js +10 -1
- package/dist/features/products/constants/final-sale.d.ts +40 -0
- package/dist/features/products/constants/final-sale.js +40 -0
- package/dist/features/products/index.d.ts +1 -0
- package/dist/features/products/index.js +1 -0
- package/dist/features/products/schemas/firestore.d.ts +19 -2
- package/dist/features/products/schemas/firestore.js +4 -0
- package/dist/features/products/types/index.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.d.ts +7 -0
- package/dist/features/seller/components/SellerProductShell.js +82 -11
- package/dist/features/seller/index.d.ts +1 -0
- package/dist/features/seller/index.js +1 -0
- package/dist/features/seller/schemas/product-form.d.ts +304 -0
- package/dist/features/seller/schemas/product-form.js +132 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +117 -0
- package/dist/features/seller/utils/product-draft-mapping.js +147 -0
- package/dist/features/shell/SectionForm.js +62 -7
- package/dist/features/shell/form-sections.js +4 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +16 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +32 -6
- package/dist/features/tester/seed-data/events-tester-seed-data.js +77 -5
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +23 -9
- package/dist/features/tester/seed-data/products-tester-seed-data.js +65 -49
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +182 -0
- package/dist/features/tester/seed-data/tester-window.d.ts +25 -0
- package/dist/features/tester/seed-data/tester-window.js +66 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +12 -3
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/schemas/registry.d.ts +271 -0
- package/dist/schemas/registry.js +9 -0
- package/dist/seed/_helpers/final-sale-fixtures.d.ts +35 -0
- package/dist/seed/_helpers/final-sale-fixtures.js +61 -0
- package/dist/seed/products-art-seed-data.js +9 -1
- package/dist/seed/products-auctions-seed-data.js +2 -1
- package/dist/seed/products-classifieds-seed-data.js +2 -1
- package/dist/seed/products-digital-codes-seed-data.js +2 -1
- package/dist/seed/products-live-items-seed-data.js +2 -1
- package/dist/seed/products-preorders-seed-data.d.ts +2 -138
- package/dist/seed/products-preorders-seed-data.js +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +2 -1
- package/dist/seed/products-standard-seed-data.js +2 -1
- package/dist/seed/products-stickers-seed-data.js +9 -1
- package/dist/seed/users-seed-data.js +107 -4
- package/dist/server.d.ts +6 -2
- package/dist/server.js +15 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/motion.d.ts +12 -0
- package/dist/tokens/motion.js +12 -0
- package/dist/ui/components/CollapsibleSection.d.ts +18 -1
- package/dist/ui/components/CollapsibleSection.js +20 -3
- package/dist/ui/components/Motion.js +17 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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:
|
|
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:
|
|
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: "
|
|
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
|
}
|
|
@@ -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();
|