Skip to content

Commit 3795836

Browse files
committed
feat: change product price to prices array
BREAKING CHANGE: Products now use `prices` array instead of singular `price`. This matches the pattern used by payment processors and allows for future support of metered pricing alongside static prices. - ProductSchema.price -> ProductSchema.prices (array) - CheckoutProductSchema.price -> CheckoutProductSchema.prices (array) - Updated all tests to use new schema structure - Bump version to 0.1.15
1 parent 133223e commit 3795836

File tree

6 files changed

+60
-45
lines changed

6 files changed

+60
-45
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@moneydevkit/api-contract",
3-
"version": "0.1.14",
3+
"version": "0.1.15",
44
"description": "API Contract for moneydevkit",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.js",

src/contracts/products.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ export const ProductPriceSchema = z.object({
99
currency: CurrencySchema,
1010
});
1111

12+
// Products have a prices array.
13+
// Currently only one static price (FIXED/CUSTOM/FREE) is supported.
14+
// Metered prices may be added in the future.
1215
export const ProductSchema = z.object({
1316
id: z.string(),
1417
name: z.string(),
1518
description: z.string().nullable(),
1619
recurringInterval: z.enum(["MONTH", "QUARTER", "YEAR"]).nullable(),
17-
price: ProductPriceSchema.nullable(),
20+
prices: z.array(ProductPriceSchema),
1821
});
1922

2023
export const ListProductsOutputSchema = z.object({

src/schemas/product.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ export const CheckoutProductPriceSchema = z.object({
88
currency: CurrencySchema,
99
});
1010

11+
// Checkout products have a prices array.
12+
// Currently only one static price (FIXED/CUSTOM/FREE) is supported.
1113
export const CheckoutProductSchema = z.object({
1214
id: z.string(),
1315
name: z.string(),
1416
description: z.string().nullable(),
1517
recurringInterval: z.enum(["MONTH", "QUARTER", "YEAR"]).nullable(),
16-
price: CheckoutProductPriceSchema.nullable(),
18+
prices: z.array(CheckoutProductPriceSchema),
1719
});

tests/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ describe('API Contract Index', () => {
5353
name: 'Test Product',
5454
description: null,
5555
recurringInterval: null,
56-
price: {
56+
prices: [{
5757
id: 'price_123',
5858
amountType: 'FIXED' as const,
5959
priceAmount: 1000,
6060
currency: 'USD',
61-
},
61+
}],
6262
}],
6363
providedAmount: null,
6464
totalAmount: null,

tests/schemas/checkout.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ const mockProduct = {
3939
name: 'Test Product',
4040
description: 'A test product',
4141
recurringInterval: null,
42-
price: {
42+
prices: [{
4343
id: 'price_123',
4444
amountType: 'FIXED' as const,
4545
priceAmount: 1000,
4646
currency: 'USD',
47-
},
47+
}],
4848
};
4949

5050
const mockInvoice = {

tests/schemas/product.test.ts

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const baseProductData = {
1616
name: "Test Product",
1717
description: null,
1818
recurringInterval: null,
19-
price: baseProductPriceData,
19+
prices: [baseProductPriceData],
2020
};
2121

2222
describe("Product Schemas", () => {
@@ -148,12 +148,14 @@ describe("Product Schemas", () => {
148148
test("should validate product with a custom price", () => {
149149
const productWithCustomPrice = {
150150
...baseProductData,
151-
price: {
152-
...baseProductPriceData,
153-
id: "price_2",
154-
amountType: "CUSTOM" as const,
155-
priceAmount: null,
156-
},
151+
prices: [
152+
{
153+
...baseProductPriceData,
154+
id: "price_2",
155+
amountType: "CUSTOM" as const,
156+
priceAmount: null,
157+
},
158+
],
157159
};
158160

159161
const result = CheckoutProductSchema.safeParse(productWithCustomPrice);
@@ -180,23 +182,23 @@ describe("Product Schemas", () => {
180182
expect(result.success).toBe(false);
181183
});
182184

183-
test("should reject product without price field", () => {
184-
const productWithoutPrice = {
185+
test("should reject product without prices field", () => {
186+
const productWithoutPrices = {
185187
...baseProductData,
186-
price: undefined,
188+
prices: undefined,
187189
};
188190

189-
const result = CheckoutProductSchema.safeParse(productWithoutPrice);
191+
const result = CheckoutProductSchema.safeParse(productWithoutPrices);
190192
expect(result.success).toBe(false);
191193
});
192194

193-
test("should allow product with null price", () => {
194-
const productWithNullPrice = {
195+
test("should validate product with empty prices array", () => {
196+
const productWithEmptyPrices = {
195197
...baseProductData,
196-
price: null,
198+
prices: [],
197199
};
198200

199-
const result = CheckoutProductSchema.safeParse(productWithNullPrice);
201+
const result = CheckoutProductSchema.safeParse(productWithEmptyPrices);
200202
expect(result.success).toBe(true);
201203
});
202204

@@ -210,13 +212,15 @@ describe("Product Schemas", () => {
210212
expect(result.success).toBe(false);
211213
});
212214

213-
test("should reject product with invalid price object", () => {
215+
test("should reject product with invalid price in prices array", () => {
214216
const productWithInvalidPrice = {
215217
...baseProductData,
216-
price: {
217-
...baseProductPriceData,
218-
amountType: "INVALID_TYPE" as any,
219-
},
218+
prices: [
219+
{
220+
...baseProductPriceData,
221+
amountType: "INVALID_TYPE" as any,
222+
},
223+
],
220224
};
221225

222226
const result = CheckoutProductSchema.safeParse(productWithInvalidPrice);
@@ -250,32 +254,38 @@ describe("Product Schemas", () => {
250254
{
251255
...baseProductData,
252256
id: "product_fixed",
253-
price: {
254-
...baseProductPriceData,
255-
id: "price_fixed",
256-
amountType: "FIXED" as const,
257-
priceAmount: 2999,
258-
},
257+
prices: [
258+
{
259+
...baseProductPriceData,
260+
id: "price_fixed",
261+
amountType: "FIXED" as const,
262+
priceAmount: 2999,
263+
},
264+
],
259265
},
260266
{
261267
...baseProductData,
262268
id: "product_custom",
263-
price: {
264-
...baseProductPriceData,
265-
id: "price_custom",
266-
amountType: "CUSTOM" as const,
267-
priceAmount: null,
268-
},
269+
prices: [
270+
{
271+
...baseProductPriceData,
272+
id: "price_custom",
273+
amountType: "CUSTOM" as const,
274+
priceAmount: null,
275+
},
276+
],
269277
},
270278
{
271279
...baseProductData,
272280
id: "product_free",
273-
price: {
274-
...baseProductPriceData,
275-
id: "price_free",
276-
amountType: "FREE" as const,
277-
priceAmount: 0,
278-
},
281+
prices: [
282+
{
283+
...baseProductPriceData,
284+
id: "price_free",
285+
amountType: "FREE" as const,
286+
priceAmount: 0,
287+
},
288+
],
279289
},
280290
];
281291

0 commit comments

Comments
 (0)