diff --git a/package.json b/package.json index c119af9..af3f358 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moneydevkit/api-contract", - "version": "0.1.14", + "version": "0.1.15", "description": "API Contract for moneydevkit", "main": "./dist/index.cjs", "module": "./dist/index.js", diff --git a/src/contracts/products.ts b/src/contracts/products.ts index 60166e5..c293838 100644 --- a/src/contracts/products.ts +++ b/src/contracts/products.ts @@ -9,12 +9,15 @@ export const ProductPriceSchema = z.object({ currency: CurrencySchema, }); +// Products have a prices array to allow future support of metered pricing +// (e.g., base subscription + usage-based charges). Currently only one static price +// (FIXED/CUSTOM/FREE) is supported. export const ProductSchema = z.object({ id: z.string(), name: z.string(), description: z.string().nullable(), recurringInterval: z.enum(["MONTH", "QUARTER", "YEAR"]).nullable(), - price: ProductPriceSchema.nullable(), + prices: z.array(ProductPriceSchema), }); export const ListProductsOutputSchema = z.object({ diff --git a/src/schemas/product.ts b/src/schemas/product.ts index 4e7edef..9e69f1f 100644 --- a/src/schemas/product.ts +++ b/src/schemas/product.ts @@ -8,10 +8,13 @@ export const CheckoutProductPriceSchema = z.object({ currency: CurrencySchema, }); +// Checkout products have a prices array to allow future support of metered pricing +// (e.g., base subscription + usage-based charges). Currently only one static price +// (FIXED/CUSTOM/FREE) is supported. export const CheckoutProductSchema = z.object({ id: z.string(), name: z.string(), description: z.string().nullable(), recurringInterval: z.enum(["MONTH", "QUARTER", "YEAR"]).nullable(), - price: CheckoutProductPriceSchema.nullable(), + prices: z.array(CheckoutProductPriceSchema), }); diff --git a/tests/index.test.ts b/tests/index.test.ts index 8b67202..a17ef18 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -53,12 +53,12 @@ describe('API Contract Index', () => { name: 'Test Product', description: null, recurringInterval: null, - price: { + prices: [{ id: 'price_123', amountType: 'FIXED' as const, priceAmount: 1000, currency: 'USD', - }, + }], }], providedAmount: null, totalAmount: null, diff --git a/tests/schemas/checkout.test.ts b/tests/schemas/checkout.test.ts index 876743b..66ee657 100644 --- a/tests/schemas/checkout.test.ts +++ b/tests/schemas/checkout.test.ts @@ -39,12 +39,12 @@ const mockProduct = { name: 'Test Product', description: 'A test product', recurringInterval: null, - price: { + prices: [{ id: 'price_123', amountType: 'FIXED' as const, priceAmount: 1000, currency: 'USD', - }, + }], }; const mockInvoice = { diff --git a/tests/schemas/product.test.ts b/tests/schemas/product.test.ts index b6eac75..dd90f44 100644 --- a/tests/schemas/product.test.ts +++ b/tests/schemas/product.test.ts @@ -16,7 +16,7 @@ const baseProductData = { name: "Test Product", description: null, recurringInterval: null, - price: baseProductPriceData, + prices: [baseProductPriceData], }; describe("Product Schemas", () => { @@ -148,12 +148,14 @@ describe("Product Schemas", () => { test("should validate product with a custom price", () => { const productWithCustomPrice = { ...baseProductData, - price: { - ...baseProductPriceData, - id: "price_2", - amountType: "CUSTOM" as const, - priceAmount: null, - }, + prices: [ + { + ...baseProductPriceData, + id: "price_2", + amountType: "CUSTOM" as const, + priceAmount: null, + }, + ], }; const result = CheckoutProductSchema.safeParse(productWithCustomPrice); @@ -180,23 +182,23 @@ describe("Product Schemas", () => { expect(result.success).toBe(false); }); - test("should reject product without price field", () => { - const productWithoutPrice = { + test("should reject product without prices field", () => { + const productWithoutPrices = { ...baseProductData, - price: undefined, + prices: undefined, }; - const result = CheckoutProductSchema.safeParse(productWithoutPrice); + const result = CheckoutProductSchema.safeParse(productWithoutPrices); expect(result.success).toBe(false); }); - test("should allow product with null price", () => { - const productWithNullPrice = { + test("should validate product with empty prices array", () => { + const productWithEmptyPrices = { ...baseProductData, - price: null, + prices: [], }; - const result = CheckoutProductSchema.safeParse(productWithNullPrice); + const result = CheckoutProductSchema.safeParse(productWithEmptyPrices); expect(result.success).toBe(true); }); @@ -210,13 +212,15 @@ describe("Product Schemas", () => { expect(result.success).toBe(false); }); - test("should reject product with invalid price object", () => { + test("should reject product with invalid price in prices array", () => { const productWithInvalidPrice = { ...baseProductData, - price: { - ...baseProductPriceData, - amountType: "INVALID_TYPE" as any, - }, + prices: [ + { + ...baseProductPriceData, + amountType: "INVALID_TYPE" as any, + }, + ], }; const result = CheckoutProductSchema.safeParse(productWithInvalidPrice); @@ -250,32 +254,38 @@ describe("Product Schemas", () => { { ...baseProductData, id: "product_fixed", - price: { - ...baseProductPriceData, - id: "price_fixed", - amountType: "FIXED" as const, - priceAmount: 2999, - }, + prices: [ + { + ...baseProductPriceData, + id: "price_fixed", + amountType: "FIXED" as const, + priceAmount: 2999, + }, + ], }, { ...baseProductData, id: "product_custom", - price: { - ...baseProductPriceData, - id: "price_custom", - amountType: "CUSTOM" as const, - priceAmount: null, - }, + prices: [ + { + ...baseProductPriceData, + id: "price_custom", + amountType: "CUSTOM" as const, + priceAmount: null, + }, + ], }, { ...baseProductData, id: "product_free", - price: { - ...baseProductPriceData, - id: "price_free", - amountType: "FREE" as const, - priceAmount: 0, - }, + prices: [ + { + ...baseProductPriceData, + id: "price_free", + amountType: "FREE" as const, + priceAmount: 0, + }, + ], }, ];