diff --git a/package.json b/package.json index b53d665..adc15b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moneydevkit/api-contract", - "version": "0.1.16", + "version": "0.1.17", "description": "API Contract for moneydevkit", "main": "./dist/index.cjs", "module": "./dist/index.js", diff --git a/src/contracts/checkout.ts b/src/contracts/checkout.ts index 2349ed2..2c6b848 100644 --- a/src/contracts/checkout.ts +++ b/src/contracts/checkout.ts @@ -72,6 +72,14 @@ export const ConfirmCheckoutInputSchema = z.object({ * Customer data provided at confirm time. */ customer: CustomerInputSchema.optional(), + /** + * Product selection at confirm time. + * - undefined or [] = keep current selection + * - [{ productId }] = change selection to this product + * - priceAmount required if selected price has amountType: CUSTOM + * + * Currently limited to single selection (max 1 item). + */ products: z .array( z.object({ @@ -79,6 +87,7 @@ export const ConfirmCheckoutInputSchema = z.object({ priceAmount: z.number().optional(), }), ) + .max(1) .optional(), }); diff --git a/src/contracts/products.ts b/src/contracts/products.ts index c293838..faba3ea 100644 --- a/src/contracts/products.ts +++ b/src/contracts/products.ts @@ -4,14 +4,14 @@ import { CurrencySchema } from "../schemas/currency"; export const ProductPriceSchema = z.object({ id: z.string(), - amountType: z.enum(["FIXED", "CUSTOM", "FREE"]), + amountType: z.enum(["FIXED", "CUSTOM"]), priceAmount: z.number().nullable(), 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. +// (FIXED/CUSTOM) is supported. export const ProductSchema = z.object({ id: z.string(), name: z.string(), diff --git a/src/schemas/checkout.ts b/src/schemas/checkout.ts index 1e90a21..1454e85 100644 --- a/src/schemas/checkout.ts +++ b/src/schemas/checkout.ts @@ -58,6 +58,29 @@ const BaseCheckoutSchema = z.object({ customer: CustomerOutputSchema.nullable(), customerBillingAddress: z.record(z.any()).nullable(), products: z.array(CheckoutProductSchema).nullable(), + /** + * The selected product ID (from the products array). + * For PRODUCTS checkouts, this is the product the customer has chosen. + * null for AMOUNT/TOP_UP checkouts. + */ + productId: z.string().nullable(), + /** + * The selected product price ID. + * References a price from the selected product's prices array. + * null for AMOUNT/TOP_UP checkouts. + */ + productPriceId: z.string().nullable(), + /** + * User-provided amount for CUSTOM price products. + * Only set when the selected price has amountType: CUSTOM. + */ + customAmount: z.number().nullable(), + /** + * The selected product with full details (convenience field). + * Same shape as items in the products array. + * null if no product is selected. + */ + product: CheckoutProductSchema.nullable(), providedAmount: z.number().nullable(), totalAmount: z.number().nullable(), discountAmount: z.number().nullable(), diff --git a/src/schemas/product.ts b/src/schemas/product.ts index 9e69f1f..c64c201 100644 --- a/src/schemas/product.ts +++ b/src/schemas/product.ts @@ -3,14 +3,14 @@ import { CurrencySchema } from "./currency"; export const CheckoutProductPriceSchema = z.object({ id: z.string(), - amountType: z.enum(["FIXED", "CUSTOM", "FREE"]), + amountType: z.enum(["FIXED", "CUSTOM"]), priceAmount: z.number().nullable(), 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. +// (FIXED/CUSTOM) is supported. export const CheckoutProductSchema = z.object({ id: z.string(), name: z.string(), diff --git a/tests/contracts/checkout.test.ts b/tests/contracts/checkout.test.ts index 4336b23..997b968 100644 --- a/tests/contracts/checkout.test.ts +++ b/tests/contracts/checkout.test.ts @@ -205,9 +205,6 @@ describe('Checkout Contracts', () => { productId: 'product_1', priceAmount: 500, }, - { - productId: 'product_2', - }, ], }; @@ -230,8 +227,7 @@ describe('Checkout Contracts', () => { const input = { checkoutId: 'checkout_123', products: [ - { productId: 'product_1' }, - { productId: 'product_2', priceAmount: 1000 }, + { productId: 'product_1', priceAmount: 1000 }, ], }; @@ -239,6 +235,19 @@ describe('Checkout Contracts', () => { expect(result.success).toBe(true); }); + test('should reject products array with more than 1 item', () => { + const input = { + checkoutId: 'checkout_123', + products: [ + { productId: 'product_1' }, + { productId: 'product_2' }, + ], + }; + + const result = ConfirmCheckoutInputSchema.safeParse(input); + expect(result.success).toBe(false); + }); + test('should accept custom fields from confirm input (form fields)', () => { // Custom fields are accepted at confirm time - they come from the form const input = { diff --git a/tests/index.test.ts b/tests/index.test.ts index a17ef18..d7beb3c 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -60,6 +60,10 @@ describe('API Contract Index', () => { currency: 'USD', }], }], + productId: null, + productPriceId: null, + customAmount: null, + product: null, providedAmount: null, totalAmount: null, discountAmount: null, diff --git a/tests/schemas/checkout.test.ts b/tests/schemas/checkout.test.ts index 66ee657..a90d4a3 100644 --- a/tests/schemas/checkout.test.ts +++ b/tests/schemas/checkout.test.ts @@ -23,6 +23,10 @@ const baseCheckoutData = { customer: null, customerBillingAddress: null, products: null, + productId: null, + productPriceId: null, + customAmount: null, + product: null, providedAmount: null, totalAmount: null, discountAmount: null, diff --git a/tests/schemas/product.test.ts b/tests/schemas/product.test.ts index dd90f44..ebe61b5 100644 --- a/tests/schemas/product.test.ts +++ b/tests/schemas/product.test.ts @@ -43,7 +43,7 @@ describe("Product Schemas", () => { expect(result.success).toBe(true); }); - test("should validate price with FREE amount type", () => { + test("should reject FREE amount type (not supported)", () => { const freePrice = { ...baseProductPriceData, amountType: "FREE" as const, @@ -51,10 +51,10 @@ describe("Product Schemas", () => { }; const result = CheckoutProductPriceSchema.safeParse(freePrice); - expect(result.success).toBe(true); + expect(result.success).toBe(false); }); - test("should reject METERED amount type", () => { + test("should reject METERED amount type (not supported)", () => { const meteredPrice = { ...baseProductPriceData, amountType: "METERED" as const, @@ -275,18 +275,6 @@ describe("Product Schemas", () => { }, ], }, - { - ...baseProductData, - id: "product_free", - prices: [ - { - ...baseProductPriceData, - id: "price_free", - amountType: "FREE" as const, - priceAmount: 0, - }, - ], - }, ]; products.forEach((product) => {