Skip to content

Commit 0140061

Browse files
committed
feat(schema): introduces the product variant operations
1 parent f13580c commit 0140061

7 files changed

Lines changed: 456 additions & 86 deletions

File tree

‎components/schema/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@crystallize/schema",
3-
"version": "6.11.0",
3+
"version": "6.12.0",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

‎components/schema/src/mass-operation/index.ts‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ import {
2626
UpsertShapeOperationSchema,
2727
} from './shape';
2828

29-
import { ModifyProductVariantStockOperationSchema } from './product-variant';
29+
import {
30+
AddProductVariantOperationSchema,
31+
DeleteProductVariantOperationSchema,
32+
ModifyProductVariantPriceOperationSchema,
33+
ModifyProductVariantStockOperationSchema,
34+
UpdateProductVariantOperationSchema,
35+
UpsertProductVariantOperationSchema,
36+
} from './product-variant';
3037
import {
3138
CreateCustomerGroupOperationSchema,
3239
CreateCustomerOperationSchema,
@@ -55,7 +62,6 @@ import {
5562
UpsertTopicOperationSchema,
5663
DeleteTopicOperationSchema,
5764
} from './topic';
58-
import { ModifyProductVariantPriceOperationSchema } from './product-variant';
5965
import { CreateFlowOperationSchema, UpdateFlowOperationSchema, UpsertFlowOperationSchema } from './flow';
6066
import {
6167
RemoveItemTreeNodeHistoryOperationSchema,
@@ -100,6 +106,10 @@ export const OperationSchema = z.discriminatedUnion('intent', [
100106

101107
ModifyProductVariantStockOperationSchema,
102108
ModifyProductVariantPriceOperationSchema,
109+
AddProductVariantOperationSchema,
110+
UpdateProductVariantOperationSchema,
111+
UpsertProductVariantOperationSchema,
112+
DeleteProductVariantOperationSchema,
103113
RegisterImageOperationSchema,
104114

105115
CreateCustomerGroupOperationSchema,
@@ -195,7 +205,14 @@ export type {
195205
UpsertPieceOperation,
196206
} from './shape';
197207

198-
export type { ModifyProductVariantStockOperation } from './product-variant';
208+
export type {
209+
ModifyProductVariantStockOperation,
210+
ModifyProductVariantPriceOperation,
211+
AddProductVariantOperation,
212+
UpdateProductVariantOperation,
213+
UpsertProductVariantOperation,
214+
DeleteProductVariantOperation,
215+
} from './product-variant';
199216

200217
export type {
201218
CreateCustomerOperation,

‎components/schema/src/mass-operation/product-variant.ts‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from 'zod';
2-
import { ModifyProductVariantPriceInputSchema, ModifyProductVariantStockInputSchema } from '../pim';
3-
import { RefSchema } from '../shared';
2+
import { ModifyProductVariantPriceInputSchema, ModifyProductVariantStockInputSchema, VariantInputSchema } from '../pim';
3+
import { IdSchema, RefSchema } from '../shared';
44

55
export const ModifyProductVariantStockOperationSchema = ModifyProductVariantStockInputSchema.extend({
66
_ref: RefSchema.optional(),
@@ -15,3 +15,36 @@ export const ModifyProductVariantPriceOperationSchema = ModifyProductVariantPric
1515
});
1616

1717
export type ModifyProductVariantPriceOperation = z.infer<typeof ModifyProductVariantPriceOperationSchema>;
18+
19+
export const AddProductVariantOperationSchema = VariantInputSchema.extend({
20+
_ref: RefSchema.optional(),
21+
intent: z.literal('product/variant/create'),
22+
language: z.string().min(1),
23+
productId: IdSchema,
24+
});
25+
export type AddProductVariantOperation = z.infer<typeof AddProductVariantOperationSchema>;
26+
27+
export const UpdateProductVariantOperationSchema = VariantInputSchema.omit({ sku: true, isDefault: true })
28+
.partial()
29+
.extend({
30+
_ref: RefSchema.optional(),
31+
intent: z.literal('product/variant/update'),
32+
language: z.string().min(1),
33+
sku: z.string().min(1),
34+
});
35+
export type UpdateProductVariantOperation = z.infer<typeof UpdateProductVariantOperationSchema>;
36+
37+
export const UpsertProductVariantOperationSchema = VariantInputSchema.extend({
38+
_ref: RefSchema.optional(),
39+
intent: z.literal('product/variant/upsert'),
40+
language: z.string().min(1),
41+
productId: IdSchema,
42+
});
43+
export type UpsertProductVariantOperation = z.infer<typeof UpsertProductVariantOperationSchema>;
44+
45+
export const DeleteProductVariantOperationSchema = z.object({
46+
_ref: RefSchema.optional(),
47+
intent: z.literal('product/variant/delete'),
48+
sku: z.string().min(1),
49+
});
50+
export type DeleteProductVariantOperation = z.infer<typeof DeleteProductVariantOperationSchema>;

‎components/schema/src/pim/items/product/product-input.ts‎

Lines changed: 2 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,7 @@
11
import { z } from 'zod';
2-
import { CreateItemInputSchema, UpdateItemInputSchema, UpdatetemInputSchema } from '../item-input';
2+
import { CreateItemInputSchema, UpdateItemInputSchema } from '../item-input';
33
import { IdSchema } from '../../../shared';
4-
import { ComponentContentInputSchema, ImageContentInputSchema, VideoContentInputSchema } from '../../components';
5-
import { KeyValueInputSchema } from '../../../shop';
6-
import { SubscriptionContractTierTypeSchema } from '../../subscription-contracts';
7-
8-
const PhaseInputSchema = z.object({
9-
meteredVariables: z.array(
10-
z.object({
11-
id: IdSchema,
12-
tierType: SubscriptionContractTierTypeSchema,
13-
tiers: z.array(
14-
z.object({
15-
price: z.number(),
16-
threshold: z.number(),
17-
priceVariants: z.array(
18-
z.object({
19-
identifier: z.string(),
20-
price: z.number(),
21-
}),
22-
),
23-
}),
24-
),
25-
}),
26-
),
27-
priceVariants: z.array(
28-
z.object({
29-
identifier: z.string(),
30-
price: z.number(),
31-
}),
32-
),
33-
});
34-
35-
export const VariantInputSchema = z.object({
36-
sku: z.string(),
37-
externalReference: z.string().nullish(),
38-
components: z.array(ComponentContentInputSchema).nullish(),
39-
attributes: z
40-
.array(
41-
z.object({
42-
attribute: z.string(),
43-
value: z.string(),
44-
}),
45-
)
46-
.nullish(),
47-
isDefault: z.boolean().nullish(),
48-
name: z.string(),
49-
images: z.array(ImageContentInputSchema).nullish(),
50-
videos: z.array(VideoContentInputSchema).nullish(),
51-
priceVariants: z
52-
.array(
53-
z.object({
54-
identifier: z.string(),
55-
price: z.number(),
56-
}),
57-
)
58-
.nullish(),
59-
stockLocations: z
60-
.array(
61-
z.object({
62-
identifier: z.string(),
63-
stock: z.number(),
64-
meta: z.array(KeyValueInputSchema).nullish(),
65-
}),
66-
)
67-
.nullish(),
68-
subscriptionPlans: z
69-
.array(
70-
z.object({
71-
identifier: z.string(),
72-
periods: z.array(
73-
z.object({
74-
id: IdSchema,
75-
initial: PhaseInputSchema.nullish(),
76-
recurring: PhaseInputSchema,
77-
}),
78-
),
79-
}),
80-
)
81-
.nullish(),
82-
});
4+
import { VariantInputSchema } from '../../product-variants';
835

846
export const CreateProductInputSchema = CreateItemInputSchema.extend({
857
vatTypeId: IdSchema,

‎components/schema/src/pim/product-variants/index.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { z } from 'zod';
2+
import { ComponentContentInputSchema, ImageContentInputSchema, VideoContentInputSchema } from '../components';
3+
import { KeyValueInputSchema } from '../../shop';
4+
import { IdSchema } from '../../shared';
5+
import { SubscriptionContractTierTypeSchema } from '../subscription-contracts';
26

37
export const ModifyProductVariantStockInputSchema = z.object({
48
operation: z.enum(['decrease', 'increase', 'overwrite']),
@@ -14,3 +18,67 @@ export const ModifyProductVariantPriceInputSchema = z.object({
1418
price: z.number().min(1),
1519
});
1620
export type ModifyProductVariantPriceInput = z.infer<typeof ModifyProductVariantPriceInputSchema>;
21+
22+
export const ProductVariantAttributeInputSchema = z.object({
23+
attribute: z.string(),
24+
value: z.string(),
25+
});
26+
export type ProductVariantAttributeInput = z.infer<typeof ProductVariantAttributeInputSchema>;
27+
28+
export const PriceVariantReferenceInputSchema = z.object({
29+
identifier: z.string(),
30+
price: z.number(),
31+
});
32+
export type PriceVariantReferenceInput = z.infer<typeof PriceVariantReferenceInputSchema>;
33+
34+
export const StockLocationReferenceInputSchema = z.object({
35+
identifier: z.string(),
36+
stock: z.number(),
37+
meta: z.array(KeyValueInputSchema).nullish(),
38+
});
39+
export type StockLocationReferenceInput = z.infer<typeof StockLocationReferenceInputSchema>;
40+
41+
export const SubscriptionPlanPhaseInputSchema = z.object({
42+
meteredVariables: z.array(
43+
z.object({
44+
id: IdSchema,
45+
tierType: SubscriptionContractTierTypeSchema,
46+
tiers: z.array(
47+
z.object({
48+
price: z.number(),
49+
threshold: z.number(),
50+
priceVariants: z.array(PriceVariantReferenceInputSchema),
51+
}),
52+
),
53+
}),
54+
),
55+
priceVariants: z.array(PriceVariantReferenceInputSchema),
56+
});
57+
export type SubscriptionPlanPhaseInput = z.infer<typeof SubscriptionPlanPhaseInputSchema>;
58+
59+
export const SubscriptionPlanReferenceInputSchema = z.object({
60+
identifier: z.string(),
61+
periods: z.array(
62+
z.object({
63+
id: IdSchema,
64+
initial: SubscriptionPlanPhaseInputSchema.nullish(),
65+
recurring: SubscriptionPlanPhaseInputSchema,
66+
}),
67+
),
68+
});
69+
export type SubscriptionPlanReferenceInput = z.infer<typeof SubscriptionPlanReferenceInputSchema>;
70+
71+
export const VariantInputSchema = z.object({
72+
sku: z.string(),
73+
externalReference: z.string().nullish(),
74+
components: z.array(ComponentContentInputSchema).nullish(),
75+
attributes: z.array(ProductVariantAttributeInputSchema).nullish(),
76+
isDefault: z.boolean().nullish(),
77+
name: z.string(),
78+
images: z.array(ImageContentInputSchema).nullish(),
79+
videos: z.array(VideoContentInputSchema).nullish(),
80+
priceVariants: z.array(PriceVariantReferenceInputSchema).nullish(),
81+
stockLocations: z.array(StockLocationReferenceInputSchema).nullish(),
82+
subscriptionPlans: z.array(SubscriptionPlanReferenceInputSchema).nullish(),
83+
});
84+
export type VariantInput = z.infer<typeof VariantInputSchema>;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import {
2+
AddProductVariantOperation,
3+
AddProductVariantOperationSchema,
4+
DeleteProductVariantOperation,
5+
DeleteProductVariantOperationSchema,
6+
UpdateProductVariantOperation,
7+
UpdateProductVariantOperationSchema,
8+
UpsertProductVariantOperation,
9+
UpsertProductVariantOperationSchema,
10+
} from '../../src/mass-operation/product-variant';
11+
import { describe, expect, it } from 'vitest';
12+
13+
describe('Mass Operations - Product Variant', {}, async () => {
14+
it('accepts a minimal add variant operation', () => {
15+
const operation: AddProductVariantOperation = {
16+
intent: 'product/variant/create',
17+
language: 'en',
18+
productId: '604f7655a16b91dea030895b',
19+
sku: 'sku-1',
20+
name: 'Variant 1',
21+
};
22+
expect(AddProductVariantOperationSchema.safeParse(operation)).toEqual({
23+
success: true,
24+
data: operation,
25+
});
26+
});
27+
28+
it('accepts an add variant operation with references', () => {
29+
const operation: AddProductVariantOperation = {
30+
intent: 'product/variant/create',
31+
language: 'en',
32+
productId: '604f7655a16b91dea030895b',
33+
sku: 'sku-1',
34+
name: 'Variant 1',
35+
isDefault: true,
36+
attributes: [{ attribute: 'color', value: 'red' }],
37+
priceVariants: [{ identifier: 'default', price: 19.99 }],
38+
stockLocations: [{ identifier: 'warehouse-eu', stock: 10 }],
39+
};
40+
expect(AddProductVariantOperationSchema.safeParse(operation).success).toBe(true);
41+
});
42+
43+
it('rejects an add variant operation without sku', () => {
44+
const invalid = {
45+
intent: 'product/variant/create',
46+
language: 'en',
47+
productId: '604f7655a16b91dea030895b',
48+
name: 'Variant 1',
49+
};
50+
expect(AddProductVariantOperationSchema.safeParse(invalid).success).toBe(false);
51+
});
52+
53+
it('rejects an add variant operation without productId', () => {
54+
const invalid = {
55+
intent: 'product/variant/create',
56+
language: 'en',
57+
sku: 'sku-1',
58+
name: 'Variant 1',
59+
};
60+
expect(AddProductVariantOperationSchema.safeParse(invalid).success).toBe(false);
61+
});
62+
63+
it('accepts a partial update variant operation', () => {
64+
const operation: UpdateProductVariantOperation = {
65+
intent: 'product/variant/update',
66+
language: 'en',
67+
sku: 'sku-1',
68+
priceVariants: [{ identifier: 'default', price: 24.99 }],
69+
};
70+
expect(UpdateProductVariantOperationSchema.safeParse(operation)).toEqual({
71+
success: true,
72+
data: operation,
73+
});
74+
});
75+
76+
it('rejects an update variant operation without sku', () => {
77+
const invalid = {
78+
intent: 'product/variant/update',
79+
language: 'en',
80+
name: 'New name',
81+
};
82+
expect(UpdateProductVariantOperationSchema.safeParse(invalid).success).toBe(false);
83+
});
84+
85+
it('accepts a minimal upsert variant operation', () => {
86+
const operation: UpsertProductVariantOperation = {
87+
intent: 'product/variant/upsert',
88+
language: 'en',
89+
productId: '604f7655a16b91dea030895b',
90+
sku: 'sku-1',
91+
name: 'Variant 1',
92+
};
93+
expect(UpsertProductVariantOperationSchema.safeParse(operation)).toEqual({
94+
success: true,
95+
data: operation,
96+
});
97+
});
98+
99+
it('rejects an upsert variant operation without sku', () => {
100+
const invalid = {
101+
intent: 'product/variant/upsert',
102+
language: 'en',
103+
productId: '604f7655a16b91dea030895b',
104+
name: 'Variant 1',
105+
};
106+
expect(UpsertProductVariantOperationSchema.safeParse(invalid).success).toBe(false);
107+
});
108+
109+
it('accepts a delete variant operation', () => {
110+
const operation: DeleteProductVariantOperation = {
111+
intent: 'product/variant/delete',
112+
sku: 'sku-1',
113+
};
114+
expect(DeleteProductVariantOperationSchema.safeParse(operation)).toEqual({
115+
success: true,
116+
data: operation,
117+
});
118+
});
119+
120+
it('rejects a delete variant operation without sku', () => {
121+
const invalid = { intent: 'product/variant/delete' };
122+
expect(DeleteProductVariantOperationSchema.safeParse(invalid).success).toBe(false);
123+
});
124+
});

0 commit comments

Comments
 (0)