@aveliya/contracts 1.0.0

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.
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ export const bankInstructionsSchema = z.object({
3
+ bankName: z.string().trim().min(1), accountTitle: z.string().trim().min(1),
4
+ accountNumber: z.string().trim().min(1), instructions: z.string().trim().min(1),
5
+ });
6
+ export const checkoutConfigSchema = z.object({
7
+ paymentMethods: z.array(z.enum(['cod', 'bank-transfer'])).min(1),
8
+ bankTransfer: bankInstructionsSchema.nullable(),
9
+ }).refine(value => !value.paymentMethods.includes('bank-transfer') || value.bankTransfer !== null, 'Enabled bank transfer requires public instructions');
@@ -0,0 +1,100 @@
1
+ import { z } from 'zod';
2
+ export declare const checkoutQuoteSchema: z.ZodObject<{
3
+ items: z.ZodArray<z.ZodObject<{
4
+ productId: z.ZodString;
5
+ variantId: z.ZodString;
6
+ name: z.ZodString;
7
+ slug: z.ZodString;
8
+ imageUrl: z.ZodString;
9
+ colorName: z.ZodString;
10
+ sizeLabel: z.ZodString;
11
+ unitPrice: z.ZodNumber;
12
+ quantity: z.ZodNumber;
13
+ lineSubtotal: z.ZodNumber;
14
+ }, "strip", z.ZodTypeAny, {
15
+ quantity: number;
16
+ imageUrl: string;
17
+ slug: string;
18
+ name: string;
19
+ variantId: string;
20
+ productId: string;
21
+ colorName: string;
22
+ sizeLabel: string;
23
+ unitPrice: number;
24
+ lineSubtotal: number;
25
+ }, {
26
+ quantity: number;
27
+ imageUrl: string;
28
+ slug: string;
29
+ name: string;
30
+ variantId: string;
31
+ productId: string;
32
+ colorName: string;
33
+ sizeLabel: string;
34
+ unitPrice: number;
35
+ lineSubtotal: number;
36
+ }>, "many">;
37
+ totals: z.ZodObject<{
38
+ subtotal: z.ZodNumber;
39
+ discount: z.ZodNumber;
40
+ shipping: z.ZodNumber;
41
+ tax: z.ZodOptional<z.ZodNumber>;
42
+ total: z.ZodNumber;
43
+ }, "strip", z.ZodTypeAny, {
44
+ total: number;
45
+ subtotal: number;
46
+ discount: number;
47
+ shipping: number;
48
+ tax?: number | undefined;
49
+ }, {
50
+ total: number;
51
+ subtotal: number;
52
+ discount: number;
53
+ shipping: number;
54
+ tax?: number | undefined;
55
+ }>;
56
+ quoteFingerprint: z.ZodString;
57
+ }, "strip", z.ZodTypeAny, {
58
+ items: {
59
+ quantity: number;
60
+ imageUrl: string;
61
+ slug: string;
62
+ name: string;
63
+ variantId: string;
64
+ productId: string;
65
+ colorName: string;
66
+ sizeLabel: string;
67
+ unitPrice: number;
68
+ lineSubtotal: number;
69
+ }[];
70
+ totals: {
71
+ total: number;
72
+ subtotal: number;
73
+ discount: number;
74
+ shipping: number;
75
+ tax?: number | undefined;
76
+ };
77
+ quoteFingerprint: string;
78
+ }, {
79
+ items: {
80
+ quantity: number;
81
+ imageUrl: string;
82
+ slug: string;
83
+ name: string;
84
+ variantId: string;
85
+ productId: string;
86
+ colorName: string;
87
+ sizeLabel: string;
88
+ unitPrice: number;
89
+ lineSubtotal: number;
90
+ }[];
91
+ totals: {
92
+ total: number;
93
+ subtotal: number;
94
+ discount: number;
95
+ shipping: number;
96
+ tax?: number | undefined;
97
+ };
98
+ quoteFingerprint: string;
99
+ }>;
100
+ export type CheckoutQuote = z.infer<typeof checkoutQuoteSchema>;
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ const amount = z.number().int().nonnegative();
3
+ export const checkoutQuoteSchema = z.object({
4
+ items: z.array(z.object({
5
+ productId: z.string(), variantId: z.string(), name: z.string(), slug: z.string(),
6
+ imageUrl: z.string(), colorName: z.string(), sizeLabel: z.string(),
7
+ unitPrice: amount, quantity: z.number().int().positive(), lineSubtotal: amount,
8
+ })).min(1),
9
+ totals: z.object({
10
+ subtotal: amount, discount: amount, shipping: amount,
11
+ tax: amount.optional(), total: amount,
12
+ }),
13
+ quoteFingerprint: z.string().regex(/^[a-f0-9]{64}$/),
14
+ });
@@ -0,0 +1,6 @@
1
+ export * from './admin-products.js';
2
+ export * from './catalog-facets.js';
3
+ export * from './categories.js';
4
+ export * from './checkout-config.js';
5
+ export * from './checkout.js';
6
+ export * from './product-input.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './admin-products.js';
2
+ export * from './catalog-facets.js';
3
+ export * from './categories.js';
4
+ export * from './checkout-config.js';
5
+ export * from './checkout.js';
6
+ export * from './product-input.js';
@@ -0,0 +1,384 @@
1
+ import { z } from 'zod';
2
+ export declare const variantSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ sku: z.ZodString;
5
+ colorId: z.ZodString;
6
+ sizeId: z.ZodString;
7
+ quantity: z.ZodNumber;
8
+ imageUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
9
+ mediaId: z.ZodOptional<z.ZodString>;
10
+ active: z.ZodOptional<z.ZodBoolean>;
11
+ }, "strict", z.ZodTypeAny, {
12
+ id: string;
13
+ sku: string;
14
+ colorId: string;
15
+ sizeId: string;
16
+ quantity: number;
17
+ imageUrl?: string | undefined;
18
+ mediaId?: string | undefined;
19
+ active?: boolean | undefined;
20
+ }, {
21
+ id: string;
22
+ sku: string;
23
+ colorId: string;
24
+ sizeId: string;
25
+ quantity: number;
26
+ imageUrl?: string | undefined;
27
+ mediaId?: string | undefined;
28
+ active?: boolean | undefined;
29
+ }>;
30
+ export declare const productSchema: z.ZodEffects<z.ZodObject<{
31
+ primaryCategoryId: z.ZodOptional<z.ZodString>;
32
+ createdAt: z.ZodOptional<z.ZodString>;
33
+ updatedAt: z.ZodOptional<z.ZodString>;
34
+ id: z.ZodString;
35
+ slug: z.ZodString;
36
+ name: z.ZodString;
37
+ sku: z.ZodString;
38
+ description: z.ZodString;
39
+ shortDescription: z.ZodString;
40
+ category: z.ZodString;
41
+ subcategory: z.ZodString;
42
+ collection: z.ZodString;
43
+ audience: z.ZodString;
44
+ basePrice: z.ZodNumber;
45
+ salePrice: z.ZodOptional<z.ZodNumber>;
46
+ collections: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
47
+ active: z.ZodOptional<z.ZodBoolean>;
48
+ seoTitle: z.ZodOptional<z.ZodString>;
49
+ seoDescription: z.ZodOptional<z.ZodString>;
50
+ images: z.ZodArray<z.ZodObject<{
51
+ id: z.ZodOptional<z.ZodString>;
52
+ primary: z.ZodOptional<z.ZodBoolean>;
53
+ order: z.ZodOptional<z.ZodNumber>;
54
+ colorId: z.ZodOptional<z.ZodString>;
55
+ variantId: z.ZodOptional<z.ZodString>;
56
+ type: z.ZodOptional<z.ZodLiteral<"image">>;
57
+ url: z.ZodEffects<z.ZodString, string, string>;
58
+ alt: z.ZodString;
59
+ width: z.ZodNumber;
60
+ height: z.ZodNumber;
61
+ }, "strict", z.ZodTypeAny, {
62
+ url: string;
63
+ alt: string;
64
+ width: number;
65
+ height: number;
66
+ id?: string | undefined;
67
+ colorId?: string | undefined;
68
+ type?: "image" | undefined;
69
+ primary?: boolean | undefined;
70
+ order?: number | undefined;
71
+ variantId?: string | undefined;
72
+ }, {
73
+ url: string;
74
+ alt: string;
75
+ width: number;
76
+ height: number;
77
+ id?: string | undefined;
78
+ colorId?: string | undefined;
79
+ type?: "image" | undefined;
80
+ primary?: boolean | undefined;
81
+ order?: number | undefined;
82
+ variantId?: string | undefined;
83
+ }>, "many">;
84
+ videos: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
85
+ colors: z.ZodArray<z.ZodObject<{
86
+ id: z.ZodString;
87
+ name: z.ZodString;
88
+ hex: z.ZodString;
89
+ }, "strict", z.ZodTypeAny, {
90
+ id: string;
91
+ name: string;
92
+ hex: string;
93
+ }, {
94
+ id: string;
95
+ name: string;
96
+ hex: string;
97
+ }>, "many">;
98
+ sizes: z.ZodArray<z.ZodObject<{
99
+ id: z.ZodString;
100
+ label: z.ZodString;
101
+ }, "strict", z.ZodTypeAny, {
102
+ id: string;
103
+ label: string;
104
+ }, {
105
+ id: string;
106
+ label: string;
107
+ }>, "many">;
108
+ variants: z.ZodArray<z.ZodObject<{
109
+ id: z.ZodString;
110
+ sku: z.ZodString;
111
+ colorId: z.ZodString;
112
+ sizeId: z.ZodString;
113
+ quantity: z.ZodNumber;
114
+ imageUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
115
+ mediaId: z.ZodOptional<z.ZodString>;
116
+ active: z.ZodOptional<z.ZodBoolean>;
117
+ }, "strict", z.ZodTypeAny, {
118
+ id: string;
119
+ sku: string;
120
+ colorId: string;
121
+ sizeId: string;
122
+ quantity: number;
123
+ imageUrl?: string | undefined;
124
+ mediaId?: string | undefined;
125
+ active?: boolean | undefined;
126
+ }, {
127
+ id: string;
128
+ sku: string;
129
+ colorId: string;
130
+ sizeId: string;
131
+ quantity: number;
132
+ imageUrl?: string | undefined;
133
+ mediaId?: string | undefined;
134
+ active?: boolean | undefined;
135
+ }>, "many">;
136
+ material: z.ZodString;
137
+ careInstructions: z.ZodArray<z.ZodString, "many">;
138
+ sizeAndFit: z.ZodArray<z.ZodString, "many">;
139
+ tags: z.ZodArray<z.ZodString, "many">;
140
+ badges: z.ZodArray<z.ZodEnum<["new", "bestseller", "limited", "sale"]>, "many">;
141
+ isFeatured: z.ZodBoolean;
142
+ isNew: z.ZodBoolean;
143
+ isBestseller: z.ZodBoolean;
144
+ }, "strict", z.ZodTypeAny, {
145
+ id: string;
146
+ sku: string;
147
+ slug: string;
148
+ name: string;
149
+ description: string;
150
+ shortDescription: string;
151
+ category: string;
152
+ subcategory: string;
153
+ collection: string;
154
+ audience: string;
155
+ basePrice: number;
156
+ images: {
157
+ url: string;
158
+ alt: string;
159
+ width: number;
160
+ height: number;
161
+ id?: string | undefined;
162
+ colorId?: string | undefined;
163
+ type?: "image" | undefined;
164
+ primary?: boolean | undefined;
165
+ order?: number | undefined;
166
+ variantId?: string | undefined;
167
+ }[];
168
+ videos: string[];
169
+ colors: {
170
+ id: string;
171
+ name: string;
172
+ hex: string;
173
+ }[];
174
+ sizes: {
175
+ id: string;
176
+ label: string;
177
+ }[];
178
+ variants: {
179
+ id: string;
180
+ sku: string;
181
+ colorId: string;
182
+ sizeId: string;
183
+ quantity: number;
184
+ imageUrl?: string | undefined;
185
+ mediaId?: string | undefined;
186
+ active?: boolean | undefined;
187
+ }[];
188
+ material: string;
189
+ careInstructions: string[];
190
+ sizeAndFit: string[];
191
+ tags: string[];
192
+ badges: ("new" | "bestseller" | "limited" | "sale")[];
193
+ isFeatured: boolean;
194
+ isNew: boolean;
195
+ isBestseller: boolean;
196
+ active?: boolean | undefined;
197
+ primaryCategoryId?: string | undefined;
198
+ collections?: string[] | undefined;
199
+ seoTitle?: string | undefined;
200
+ seoDescription?: string | undefined;
201
+ createdAt?: string | undefined;
202
+ updatedAt?: string | undefined;
203
+ salePrice?: number | undefined;
204
+ }, {
205
+ id: string;
206
+ sku: string;
207
+ slug: string;
208
+ name: string;
209
+ description: string;
210
+ shortDescription: string;
211
+ category: string;
212
+ subcategory: string;
213
+ collection: string;
214
+ audience: string;
215
+ basePrice: number;
216
+ images: {
217
+ url: string;
218
+ alt: string;
219
+ width: number;
220
+ height: number;
221
+ id?: string | undefined;
222
+ colorId?: string | undefined;
223
+ type?: "image" | undefined;
224
+ primary?: boolean | undefined;
225
+ order?: number | undefined;
226
+ variantId?: string | undefined;
227
+ }[];
228
+ videos: string[];
229
+ colors: {
230
+ id: string;
231
+ name: string;
232
+ hex: string;
233
+ }[];
234
+ sizes: {
235
+ id: string;
236
+ label: string;
237
+ }[];
238
+ variants: {
239
+ id: string;
240
+ sku: string;
241
+ colorId: string;
242
+ sizeId: string;
243
+ quantity: number;
244
+ imageUrl?: string | undefined;
245
+ mediaId?: string | undefined;
246
+ active?: boolean | undefined;
247
+ }[];
248
+ material: string;
249
+ careInstructions: string[];
250
+ sizeAndFit: string[];
251
+ tags: string[];
252
+ badges: ("new" | "bestseller" | "limited" | "sale")[];
253
+ isFeatured: boolean;
254
+ isNew: boolean;
255
+ isBestseller: boolean;
256
+ active?: boolean | undefined;
257
+ primaryCategoryId?: string | undefined;
258
+ collections?: string[] | undefined;
259
+ seoTitle?: string | undefined;
260
+ seoDescription?: string | undefined;
261
+ createdAt?: string | undefined;
262
+ updatedAt?: string | undefined;
263
+ salePrice?: number | undefined;
264
+ }>, {
265
+ id: string;
266
+ sku: string;
267
+ slug: string;
268
+ name: string;
269
+ description: string;
270
+ shortDescription: string;
271
+ category: string;
272
+ subcategory: string;
273
+ collection: string;
274
+ audience: string;
275
+ basePrice: number;
276
+ images: {
277
+ url: string;
278
+ alt: string;
279
+ width: number;
280
+ height: number;
281
+ id?: string | undefined;
282
+ colorId?: string | undefined;
283
+ type?: "image" | undefined;
284
+ primary?: boolean | undefined;
285
+ order?: number | undefined;
286
+ variantId?: string | undefined;
287
+ }[];
288
+ videos: string[];
289
+ colors: {
290
+ id: string;
291
+ name: string;
292
+ hex: string;
293
+ }[];
294
+ sizes: {
295
+ id: string;
296
+ label: string;
297
+ }[];
298
+ variants: {
299
+ id: string;
300
+ sku: string;
301
+ colorId: string;
302
+ sizeId: string;
303
+ quantity: number;
304
+ imageUrl?: string | undefined;
305
+ mediaId?: string | undefined;
306
+ active?: boolean | undefined;
307
+ }[];
308
+ material: string;
309
+ careInstructions: string[];
310
+ sizeAndFit: string[];
311
+ tags: string[];
312
+ badges: ("new" | "bestseller" | "limited" | "sale")[];
313
+ isFeatured: boolean;
314
+ isNew: boolean;
315
+ isBestseller: boolean;
316
+ active?: boolean | undefined;
317
+ primaryCategoryId?: string | undefined;
318
+ collections?: string[] | undefined;
319
+ seoTitle?: string | undefined;
320
+ seoDescription?: string | undefined;
321
+ createdAt?: string | undefined;
322
+ updatedAt?: string | undefined;
323
+ salePrice?: number | undefined;
324
+ }, {
325
+ id: string;
326
+ sku: string;
327
+ slug: string;
328
+ name: string;
329
+ description: string;
330
+ shortDescription: string;
331
+ category: string;
332
+ subcategory: string;
333
+ collection: string;
334
+ audience: string;
335
+ basePrice: number;
336
+ images: {
337
+ url: string;
338
+ alt: string;
339
+ width: number;
340
+ height: number;
341
+ id?: string | undefined;
342
+ colorId?: string | undefined;
343
+ type?: "image" | undefined;
344
+ primary?: boolean | undefined;
345
+ order?: number | undefined;
346
+ variantId?: string | undefined;
347
+ }[];
348
+ videos: string[];
349
+ colors: {
350
+ id: string;
351
+ name: string;
352
+ hex: string;
353
+ }[];
354
+ sizes: {
355
+ id: string;
356
+ label: string;
357
+ }[];
358
+ variants: {
359
+ id: string;
360
+ sku: string;
361
+ colorId: string;
362
+ sizeId: string;
363
+ quantity: number;
364
+ imageUrl?: string | undefined;
365
+ mediaId?: string | undefined;
366
+ active?: boolean | undefined;
367
+ }[];
368
+ material: string;
369
+ careInstructions: string[];
370
+ sizeAndFit: string[];
371
+ tags: string[];
372
+ badges: ("new" | "bestseller" | "limited" | "sale")[];
373
+ isFeatured: boolean;
374
+ isNew: boolean;
375
+ isBestseller: boolean;
376
+ active?: boolean | undefined;
377
+ primaryCategoryId?: string | undefined;
378
+ collections?: string[] | undefined;
379
+ seoTitle?: string | undefined;
380
+ seoDescription?: string | undefined;
381
+ createdAt?: string | undefined;
382
+ updatedAt?: string | undefined;
383
+ salePrice?: number | undefined;
384
+ }>;
@@ -0,0 +1,150 @@
1
+ import { z } from 'zod';
2
+ const id = z
3
+ .string()
4
+ .trim()
5
+ .min(1)
6
+ .max(120)
7
+ .regex(/^[a-zA-Z0-9_-]+$/);
8
+ const classification = z
9
+ .string()
10
+ .trim()
11
+ .min(1)
12
+ .max(180)
13
+ .regex(/^[a-zA-Z0-9_-]+$/);
14
+ const money = z.number().int().min(0).max(10000000);
15
+ const url = z
16
+ .string()
17
+ .url()
18
+ .max(2048)
19
+ .refine((v) => /^https?:\/\//.test(v), 'HTTP(S) URL required');
20
+ const list = z.array(z.string().trim().min(1).max(100)).max(50);
21
+ export const variantSchema = z
22
+ .object({
23
+ id,
24
+ sku: id,
25
+ colorId: id,
26
+ sizeId: id,
27
+ quantity: z.number().int().min(0).max(1000000),
28
+ imageUrl: url.optional(),
29
+ mediaId: id.optional(),
30
+ active: z.boolean().optional(),
31
+ })
32
+ .strict();
33
+ export const productSchema = z
34
+ .object({
35
+ primaryCategoryId: id.optional(),
36
+ createdAt: z.string().datetime().optional(),
37
+ updatedAt: z.string().datetime().optional(),
38
+ id,
39
+ slug: z
40
+ .string()
41
+ .max(120)
42
+ .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
43
+ name: z.string().trim().min(1).max(200),
44
+ sku: id,
45
+ description: z.string().max(20000),
46
+ shortDescription: z.string().max(1000),
47
+ category: classification,
48
+ subcategory: classification,
49
+ collection: id,
50
+ audience: classification,
51
+ basePrice: money.positive(),
52
+ salePrice: money.optional(),
53
+ collections: z.array(id).min(1).max(50).optional(),
54
+ active: z.boolean().optional(),
55
+ seoTitle: z.string().max(200).optional(),
56
+ seoDescription: z.string().max(500).optional(),
57
+ images: z
58
+ .array(z
59
+ .object({
60
+ id: id.optional(),
61
+ primary: z.boolean().optional(),
62
+ order: z.number().int().min(0).max(29).optional(),
63
+ colorId: id.optional(),
64
+ variantId: id.optional(),
65
+ type: z.literal('image').optional(),
66
+ url,
67
+ alt: z.string().max(500),
68
+ width: z.number().int().positive(),
69
+ height: z.number().int().positive(),
70
+ })
71
+ .strict())
72
+ .min(1)
73
+ .max(30),
74
+ videos: z.array(url).max(10),
75
+ colors: z
76
+ .array(z
77
+ .object({
78
+ id,
79
+ name: z.string().min(1).max(100),
80
+ hex: z.string().regex(/^#[0-9a-fA-F]{6}$/),
81
+ })
82
+ .strict())
83
+ .min(1)
84
+ .max(30),
85
+ sizes: z
86
+ .array(z.object({ id, label: z.string().min(1).max(50) }).strict())
87
+ .min(1)
88
+ .max(30),
89
+ variants: z.array(variantSchema).min(1).max(900),
90
+ material: z.string().min(1).max(100),
91
+ careInstructions: list,
92
+ sizeAndFit: list,
93
+ tags: list,
94
+ badges: z.array(z.enum(['new', 'bestseller', 'limited', 'sale'])).max(4),
95
+ isFeatured: z.boolean(),
96
+ isNew: z.boolean(),
97
+ isBestseller: z.boolean(),
98
+ })
99
+ .strict()
100
+ .superRefine((p, ctx) => {
101
+ if (p.salePrice !== undefined && p.salePrice >= p.basePrice)
102
+ ctx.addIssue({
103
+ code: 'custom',
104
+ path: ['salePrice'],
105
+ message: 'Sale price must be below base price',
106
+ });
107
+ const issue = (path, message) => ctx.addIssue({ code: 'custom', path, message });
108
+ if (p.images.filter((image) => image.primary).length > 1)
109
+ issue(['images'], 'Only one primary image is allowed');
110
+ const mediaIds = p.images.flatMap((image) => (image.id ? [image.id] : []));
111
+ if (new Set(mediaIds).size !== mediaIds.length)
112
+ issue(['images'], 'Duplicate media IDs');
113
+ const orders = p.images.flatMap((image) => (image.order === undefined ? [] : [image.order]));
114
+ if (new Set(orders).size !== orders.length)
115
+ issue(['images'], 'Duplicate media ordering');
116
+ p.images.forEach((image, index) => {
117
+ if (image.colorId && !p.colors.some((color) => color.id === image.colorId))
118
+ issue(['images', index, 'colorId'], 'Unknown color');
119
+ const variant = p.variants.find((v) => v.id === image.variantId);
120
+ if (image.variantId && !variant)
121
+ issue(['images', index, 'variantId'], 'Unknown variant');
122
+ if (variant && image.colorId && variant.colorId !== image.colorId)
123
+ issue(['images', index], 'Media color does not match variant');
124
+ });
125
+ p.variants.forEach((variant, index) => {
126
+ const media = p.images.find((image) => image.id === variant.mediaId);
127
+ if (variant.mediaId &&
128
+ (!media ||
129
+ (media.variantId && media.variantId !== variant.id) ||
130
+ (media.colorId && media.colorId !== variant.colorId)))
131
+ issue(['variants', index, 'mediaId'], 'Invalid media association');
132
+ });
133
+ const unique = (values) => new Set(values).size === values.length;
134
+ if (!unique(p.colors.map((v) => v.id)) ||
135
+ !unique(p.sizes.map((v) => v.id)) ||
136
+ !unique(p.variants.map((v) => v.id)) ||
137
+ !unique(p.variants.map((v) => v.sku)) ||
138
+ !unique(p.variants.map((v) => `${v.colorId}:${v.sizeId}`)))
139
+ ctx.addIssue({
140
+ code: 'custom',
141
+ path: ['variants'],
142
+ message: 'Duplicate color, size or variant',
143
+ });
144
+ if (p.variants.some((v) => !p.colors.some((c) => c.id === v.colorId) || !p.sizes.some((s) => s.id === v.sizeId)))
145
+ ctx.addIssue({
146
+ code: 'custom',
147
+ path: ['variants'],
148
+ message: 'Variant references an unknown color or size',
149
+ });
150
+ });