Skip to content

Commit 967248c

Browse files
authored
Merge pull request #11 from moneydevkit/mdk-403
feat(checkout): add product selection fields (MDK-403)
2 parents 958e8df + fb890af commit 967248c

9 files changed

Lines changed: 62 additions & 25 deletions

File tree

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.16",
3+
"version": "0.1.17",
44
"description": "API Contract for moneydevkit",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.js",

src/contracts/checkout.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,22 @@ export const ConfirmCheckoutInputSchema = z.object({
7272
* Customer data provided at confirm time.
7373
*/
7474
customer: CustomerInputSchema.optional(),
75+
/**
76+
* Product selection at confirm time.
77+
* - undefined or [] = keep current selection
78+
* - [{ productId }] = change selection to this product
79+
* - priceAmount required if selected price has amountType: CUSTOM
80+
*
81+
* Currently limited to single selection (max 1 item).
82+
*/
7583
products: z
7684
.array(
7785
z.object({
7886
productId: z.string(),
7987
priceAmount: z.number().optional(),
8088
}),
8189
)
90+
.max(1)
8291
.optional(),
8392
});
8493

src/contracts/products.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { CurrencySchema } from "../schemas/currency";
44

55
export const ProductPriceSchema = z.object({
66
id: z.string(),
7-
amountType: z.enum(["FIXED", "CUSTOM", "FREE"]),
7+
amountType: z.enum(["FIXED", "CUSTOM"]),
88
priceAmount: z.number().nullable(),
99
currency: CurrencySchema,
1010
});
1111

1212
// Products have a prices array to allow future support of metered pricing
1313
// (e.g., base subscription + usage-based charges). Currently only one static price
14-
// (FIXED/CUSTOM/FREE) is supported.
14+
// (FIXED/CUSTOM) is supported.
1515
export const ProductSchema = z.object({
1616
id: z.string(),
1717
name: z.string(),

src/schemas/checkout.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,29 @@ const BaseCheckoutSchema = z.object({
5858
customer: CustomerOutputSchema.nullable(),
5959
customerBillingAddress: z.record(z.any()).nullable(),
6060
products: z.array(CheckoutProductSchema).nullable(),
61+
/**
62+
* The selected product ID (from the products array).
63+
* For PRODUCTS checkouts, this is the product the customer has chosen.
64+
* null for AMOUNT/TOP_UP checkouts.
65+
*/
66+
productId: z.string().nullable(),
67+
/**
68+
* The selected product price ID.
69+
* References a price from the selected product's prices array.
70+
* null for AMOUNT/TOP_UP checkouts.
71+
*/
72+
productPriceId: z.string().nullable(),
73+
/**
74+
* User-provided amount for CUSTOM price products.
75+
* Only set when the selected price has amountType: CUSTOM.
76+
*/
77+
customAmount: z.number().nullable(),
78+
/**
79+
* The selected product with full details (convenience field).
80+
* Same shape as items in the products array.
81+
* null if no product is selected.
82+
*/
83+
product: CheckoutProductSchema.nullable(),
6184
providedAmount: z.number().nullable(),
6285
totalAmount: z.number().nullable(),
6386
discountAmount: z.number().nullable(),

src/schemas/product.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { CurrencySchema } from "./currency";
33

44
export const CheckoutProductPriceSchema = z.object({
55
id: z.string(),
6-
amountType: z.enum(["FIXED", "CUSTOM", "FREE"]),
6+
amountType: z.enum(["FIXED", "CUSTOM"]),
77
priceAmount: z.number().nullable(),
88
currency: CurrencySchema,
99
});
1010

1111
// Checkout products have a prices array to allow future support of metered pricing
1212
// (e.g., base subscription + usage-based charges). Currently only one static price
13-
// (FIXED/CUSTOM/FREE) is supported.
13+
// (FIXED/CUSTOM) is supported.
1414
export const CheckoutProductSchema = z.object({
1515
id: z.string(),
1616
name: z.string(),

tests/contracts/checkout.test.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,6 @@ describe('Checkout Contracts', () => {
205205
productId: 'product_1',
206206
priceAmount: 500,
207207
},
208-
{
209-
productId: 'product_2',
210-
},
211208
],
212209
};
213210

@@ -230,15 +227,27 @@ describe('Checkout Contracts', () => {
230227
const input = {
231228
checkoutId: 'checkout_123',
232229
products: [
233-
{ productId: 'product_1' },
234-
{ productId: 'product_2', priceAmount: 1000 },
230+
{ productId: 'product_1', priceAmount: 1000 },
235231
],
236232
};
237233

238234
const result = ConfirmCheckoutInputSchema.safeParse(input);
239235
expect(result.success).toBe(true);
240236
});
241237

238+
test('should reject products array with more than 1 item', () => {
239+
const input = {
240+
checkoutId: 'checkout_123',
241+
products: [
242+
{ productId: 'product_1' },
243+
{ productId: 'product_2' },
244+
],
245+
};
246+
247+
const result = ConfirmCheckoutInputSchema.safeParse(input);
248+
expect(result.success).toBe(false);
249+
});
250+
242251
test('should accept custom fields from confirm input (form fields)', () => {
243252
// Custom fields are accepted at confirm time - they come from the form
244253
const input = {

tests/index.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ describe('API Contract Index', () => {
6060
currency: 'USD',
6161
}],
6262
}],
63+
productId: null,
64+
productPriceId: null,
65+
customAmount: null,
66+
product: null,
6367
providedAmount: null,
6468
totalAmount: null,
6569
discountAmount: null,

tests/schemas/checkout.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ const baseCheckoutData = {
2323
customer: null,
2424
customerBillingAddress: null,
2525
products: null,
26+
productId: null,
27+
productPriceId: null,
28+
customAmount: null,
29+
product: null,
2630
providedAmount: null,
2731
totalAmount: null,
2832
discountAmount: null,

tests/schemas/product.test.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,18 @@ describe("Product Schemas", () => {
4343
expect(result.success).toBe(true);
4444
});
4545

46-
test("should validate price with FREE amount type", () => {
46+
test("should reject FREE amount type (not supported)", () => {
4747
const freePrice = {
4848
...baseProductPriceData,
4949
amountType: "FREE" as const,
5050
priceAmount: 0,
5151
};
5252

5353
const result = CheckoutProductPriceSchema.safeParse(freePrice);
54-
expect(result.success).toBe(true);
54+
expect(result.success).toBe(false);
5555
});
5656

57-
test("should reject METERED amount type", () => {
57+
test("should reject METERED amount type (not supported)", () => {
5858
const meteredPrice = {
5959
...baseProductPriceData,
6060
amountType: "METERED" as const,
@@ -275,18 +275,6 @@ describe("Product Schemas", () => {
275275
},
276276
],
277277
},
278-
{
279-
...baseProductData,
280-
id: "product_free",
281-
prices: [
282-
{
283-
...baseProductPriceData,
284-
id: "price_free",
285-
amountType: "FREE" as const,
286-
priceAmount: 0,
287-
},
288-
],
289-
},
290278
];
291279

292280
products.forEach((product) => {

0 commit comments

Comments
 (0)