Skip to content

Commit 372433b

Browse files
refactor: add list/listPaginated pattern and reorganize schemas
- Add list (simple) + listPaginated (cursor-based) for customer, order, checkout - Move entity schemas from contracts/ to schemas/: - ProductSchema, ProductDetailSchema, ProductPriceSchema -> schemas/product.ts - CheckoutStatusSchema, CheckoutTypeSchema, CheckoutListItemSchema -> schemas/checkout.ts - OrderWithRelationsSchema -> schemas/order.ts - Create shared PaginatedInputSchema for DRY pagination inputs - Add externalId to CreateCustomerInput and UpdateCustomerInput
1 parent dacc647 commit 372433b

9 files changed

Lines changed: 430 additions & 176 deletions

File tree

src/contracts/checkout.ts

Lines changed: 64 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
import { oc } from "@orpc/contract";
22
import { z } from "zod";
3-
import { CheckoutSchema } from "../schemas/checkout";
4-
import { CurrencySchema } from "../schemas/currency";
5-
import { CustomerSchema } from "../schemas/customer";
63
import {
7-
PaginationInputSchema,
8-
PaginationOutputSchema,
9-
} from "../schemas/pagination";
4+
CheckoutSchema,
5+
CheckoutStatusSchema,
6+
CheckoutTypeSchema,
7+
CheckoutListItemSchema,
8+
CheckoutDetailSchema,
9+
type CheckoutStatus,
10+
type CheckoutType,
11+
type CheckoutListItem,
12+
type CheckoutDetail,
13+
} from "../schemas/checkout";
14+
import { CurrencySchema } from "../schemas/currency";
15+
import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination";
16+
17+
// Re-export entity schemas for backwards compatibility
18+
export {
19+
CheckoutStatusSchema,
20+
CheckoutTypeSchema,
21+
CheckoutListItemSchema,
22+
CheckoutDetailSchema,
23+
};
24+
export type { CheckoutStatus, CheckoutType, CheckoutListItem, CheckoutDetail };
1025

1126
/**
1227
* Helper to treat empty strings as undefined (not provided).
13-
* This allows clients to pass empty strings without validation errors.
1428
*/
1529
const emptyStringToUndefined = z
1630
.string()
@@ -23,22 +37,12 @@ const emailOrEmpty = z.string().email().optional().or(z.literal(""));
2337

2438
/**
2539
* Valid fields that can be required at checkout time.
26-
* - Standard fields: 'email', 'name' (checked against customer.email/name)
27-
* - Any other string is a custom field (checked against customer[field])
28-
*
29-
* @example ['email'] - require email
30-
* @example ['email', 'name'] - require both email and name
31-
* @example ['email', 'company'] - require email and company
3240
*/
3341
export const CustomerFieldSchema = z.string().min(1);
3442
export type CustomerField = string;
3543

3644
/**
37-
* Customer data object for checkout.
38-
* Flat structure - standard fields (name, email, externalId) plus any custom string fields.
39-
* Empty strings are treated as undefined (not provided).
40-
*
41-
* @example { name: "John", email: "john@example.com", externalId: "user_123", company: "Acme" }
45+
* Customer data object for checkout input.
4246
*/
4347
export const CustomerInputSchema = z
4448
.object({
@@ -50,6 +54,7 @@ export const CustomerInputSchema = z
5054

5155
export type CustomerInput = z.infer<typeof CustomerInputSchema>;
5256

57+
// Input schemas
5358
export const CreateCheckoutInputSchema = z.object({
5459
nodeId: z.string(),
5560
amount: z.number().optional(),
@@ -58,33 +63,13 @@ export const CreateCheckoutInputSchema = z.object({
5863
successUrl: z.string().optional(),
5964
allowDiscountCodes: z.boolean().optional(),
6065
metadata: z.record(z.string(), z.any()).optional(),
61-
/**
62-
* Customer data for this checkout.
63-
*/
6466
customer: CustomerInputSchema.optional(),
65-
/**
66-
* Array of customer fields to require at checkout.
67-
* If a field is listed here and not provided, the checkout UI will prompt for it.
68-
* @example ['email'] - require email
69-
* @example ['email', 'name'] - require both
70-
*/
7167
requireCustomerData: z.array(CustomerFieldSchema).optional(),
7268
});
7369

7470
export const ConfirmCheckoutInputSchema = z.object({
7571
checkoutId: z.string(),
76-
/**
77-
* Customer data provided at confirm time.
78-
*/
7972
customer: CustomerInputSchema.optional(),
80-
/**
81-
* Product selection at confirm time.
82-
* - undefined or [] = keep current selection
83-
* - [{ productId }] = change selection to this product
84-
* - priceAmount required if selected price has amountType: CUSTOM
85-
*
86-
* Currently limited to single selection (max 1 item).
87-
*/
8873
products: z
8974
.array(
9075
z.object({
@@ -120,25 +105,54 @@ export const PaymentReceivedInputSchema = z.object({
120105
),
121106
});
122107

123-
export const GetCheckoutInputSchema = z.object({ id: z.string() });
108+
export const GetCheckoutInputSchema = z.object({
109+
id: z.string().describe("The checkout ID"),
110+
});
111+
export type GetCheckoutInput = z.infer<typeof GetCheckoutInputSchema>;
124112

125113
export type CreateCheckout = z.infer<typeof CreateCheckoutInputSchema>;
126114
export type ConfirmCheckout = z.infer<typeof ConfirmCheckoutInputSchema>;
127115
export type RegisterInvoice = z.infer<typeof RegisterInvoiceInputSchema>;
128116
export type PaymentReceived = z.infer<typeof PaymentReceivedInputSchema>;
129117

118+
// List output schemas
119+
export const ListCheckoutsOutputSchema = z.object({
120+
checkouts: z.array(CheckoutSchema),
121+
});
122+
export type ListCheckoutsOutput = z.infer<typeof ListCheckoutsOutputSchema>;
123+
124+
export const ListCheckoutsPaginatedInputSchema = PaginatedInputSchema.extend({
125+
status: CheckoutStatusSchema.optional().describe("Filter by status: UNCONFIRMED, CONFIRMED, PENDING_PAYMENT, PAYMENT_RECEIVED, or EXPIRED"),
126+
});
127+
export type ListCheckoutsPaginatedInput = z.infer<typeof ListCheckoutsPaginatedInputSchema>;
128+
129+
export const ListCheckoutsPaginatedOutputSchema = PaginationOutputSchema.extend({
130+
checkouts: z.array(CheckoutSchema),
131+
});
132+
export type ListCheckoutsPaginatedOutput = z.infer<typeof ListCheckoutsPaginatedOutputSchema>;
133+
134+
export const ListCheckoutsSummaryOutputSchema = PaginationOutputSchema.extend({
135+
checkouts: z.array(CheckoutListItemSchema),
136+
});
137+
export type ListCheckoutsSummaryOutput = z.infer<typeof ListCheckoutsSummaryOutputSchema>;
138+
139+
// Contracts
130140
export const createCheckoutContract = oc
131141
.input(CreateCheckoutInputSchema)
132142
.output(CheckoutSchema);
143+
133144
export const applyDiscountCodeContract = oc
134145
.input(ApplyDiscountCodeInputSchema)
135146
.output(CheckoutSchema);
147+
136148
export const confirmCheckoutContract = oc
137149
.input(ConfirmCheckoutInputSchema)
138150
.output(CheckoutSchema);
151+
139152
export const registerInvoiceContract = oc
140153
.input(RegisterInvoiceInputSchema)
141154
.output(CheckoutSchema);
155+
142156
export const getCheckoutContract = oc
143157
.input(GetCheckoutInputSchema)
144158
.output(CheckoutSchema);
@@ -147,67 +161,19 @@ export const paymentReceivedContract = oc
147161
.input(PaymentReceivedInputSchema)
148162
.output(z.object({ ok: z.boolean() }));
149163

150-
// List checkouts schemas
151-
export const CheckoutStatusSchema = z.enum([
152-
"UNCONFIRMED",
153-
"CONFIRMED",
154-
"PENDING_PAYMENT",
155-
"PAYMENT_RECEIVED",
156-
"EXPIRED",
157-
]);
158-
export type CheckoutStatus = z.infer<typeof CheckoutStatusSchema>;
159-
160-
export const CheckoutTypeSchema = z.enum(["PRODUCTS", "AMOUNT", "TOP_UP"]);
161-
export type CheckoutType = z.infer<typeof CheckoutTypeSchema>;
162-
163-
const ListCheckoutsInputSchema = PaginationInputSchema.extend({
164-
status: CheckoutStatusSchema.optional(),
165-
});
166-
167-
const ListCheckoutsOutputSchema = PaginationOutputSchema.extend({
168-
checkouts: z.array(CheckoutSchema),
169-
});
170-
171164
export const listCheckoutsContract = oc
172-
.input(ListCheckoutsInputSchema)
165+
.input(z.object({}))
173166
.output(ListCheckoutsOutputSchema);
174167

175-
const CheckoutCustomerSchema = CustomerSchema.nullable();
176-
177-
// MCP-specific summary schema for list (simpler than full CheckoutSchema)
178-
const CheckoutListItemSchema = z.object({
179-
id: z.string(),
180-
status: CheckoutStatusSchema,
181-
type: CheckoutTypeSchema,
182-
currency: CurrencySchema,
183-
totalAmount: z.number().nullable(),
184-
customerId: z.string().nullable(),
185-
customer: CheckoutCustomerSchema,
186-
productId: z.string().nullable(),
187-
organizationId: z.string(),
188-
expiresAt: z.date(),
189-
createdAt: z.date(),
190-
modifiedAt: z.date().nullable(),
191-
});
192-
193-
// MCP-specific detailed schema for get (includes additional fields)
194-
const CheckoutDetailSchema = CheckoutListItemSchema.extend({
195-
userMetadata: z.record(z.unknown()).nullable(),
196-
successUrl: z.string().nullable(),
197-
discountAmount: z.number().nullable(),
198-
netAmount: z.number().nullable(),
199-
taxAmount: z.number().nullable(),
200-
});
201-
202-
const ListCheckoutsSummaryOutputSchema = PaginationOutputSchema.extend({
203-
checkouts: z.array(CheckoutListItemSchema),
204-
});
168+
export const listCheckoutsPaginatedContract = oc
169+
.input(ListCheckoutsPaginatedInputSchema)
170+
.output(ListCheckoutsPaginatedOutputSchema);
205171

206-
export const listCheckoutsSummaryContract = oc
207-
.input(ListCheckoutsInputSchema)
172+
export const listCheckoutsSummaryPaginatedContract = oc
173+
.input(ListCheckoutsPaginatedInputSchema)
208174
.output(ListCheckoutsSummaryOutputSchema);
209175

210-
export const getCheckoutSummaryContract = oc
176+
export const getCheckoutDetailContract = oc
211177
.input(GetCheckoutInputSchema)
212178
.output(CheckoutDetailSchema);
213179

@@ -218,6 +184,8 @@ export const checkout = {
218184
registerInvoice: registerInvoiceContract,
219185
paymentReceived: paymentReceivedContract,
220186
list: listCheckoutsContract,
221-
listSummary: listCheckoutsSummaryContract,
222-
getSummary: getCheckoutSummaryContract,
187+
listPaginated: listCheckoutsPaginatedContract,
188+
// Original names preserved
189+
listSummary: listCheckoutsSummaryPaginatedContract,
190+
getSummary: getCheckoutDetailContract,
223191
};

src/contracts/customer.ts

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,78 @@ import {
55
CustomerWithSubscriptionsSchema,
66
GetCustomerInputSchema as SdkGetCustomerInputSchema,
77
} from "../schemas/customer";
8-
import {
9-
PaginationInputSchema,
10-
PaginationOutputSchema,
11-
} from "../schemas/pagination";
8+
import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination";
9+
10+
// Simple list (no pagination)
11+
export const ListCustomersOutputSchema = z.object({
12+
customers: z.array(CustomerSchema),
13+
});
14+
export type ListCustomersOutput = z.infer<typeof ListCustomersOutputSchema>;
15+
16+
// Paginated list (no additional filters for customers)
17+
export const ListCustomersPaginatedInputSchema = PaginatedInputSchema;
18+
export type ListCustomersPaginatedInput = z.infer<typeof ListCustomersPaginatedInputSchema>;
1219

13-
// MCP-specific schemas
14-
const ListCustomersInputSchema = PaginationInputSchema;
15-
const ListCustomersOutputSchema = PaginationOutputSchema.extend({
20+
export const ListCustomersPaginatedOutputSchema = PaginationOutputSchema.extend({
1621
customers: z.array(CustomerSchema),
1722
});
23+
export type ListCustomersPaginatedOutput = z.infer<typeof ListCustomersPaginatedOutputSchema>;
1824

19-
const McpGetCustomerInputSchema = z.object({ id: z.string() });
25+
// Flexible customer lookup - exactly one of id, email, or externalId
26+
// Base shape without refinement (for MCP tool schemas)
27+
export const CustomerLookupBaseSchema = z.object({
28+
id: z.string().optional().describe("The customer ID"),
29+
email: z.string().optional().describe("The customer email address"),
30+
externalId: z.string().optional().describe("The external ID from your system"),
31+
});
32+
33+
// With refinement for runtime validation
34+
export const CustomerLookupInputSchema = CustomerLookupBaseSchema.refine(
35+
(data) => [data.id, data.email, data.externalId].filter(Boolean).length === 1,
36+
{ message: "Exactly one of id, email, or externalId must be provided" },
37+
);
38+
export type CustomerLookupInput = z.infer<typeof CustomerLookupInputSchema>;
39+
40+
// Aliases for specific operations
41+
export const GetCustomerInputSchema = CustomerLookupBaseSchema;
42+
export type GetCustomerInput = z.infer<typeof GetCustomerInputSchema>;
2043

21-
const CreateCustomerInputSchema = z.object({
22-
name: z.string().min(1),
23-
email: z.string().email(),
44+
export const DeleteCustomerInputSchema = CustomerLookupBaseSchema;
45+
export type DeleteCustomerInput = z.infer<typeof DeleteCustomerInputSchema>;
46+
47+
export const CreateCustomerInputSchema = z.object({
48+
name: z.string().min(1).describe("Customer name"),
49+
email: z.string().email().describe("Customer email address"),
50+
externalId: z.string().optional().describe("External ID from your system for linking"),
2451
});
2552

26-
const UpdateCustomerInputSchema = z.object({
27-
id: z.string(),
28-
name: z.string().optional(),
29-
email: z.string().email().optional(),
30-
userMetadata: z.record(z.string(), z.string()).optional(),
53+
export const UpdateCustomerInputSchema = z.object({
54+
id: z.string().describe("The customer ID to update"),
55+
name: z.string().optional().describe("New customer name"),
56+
email: z.string().email().optional().describe("New customer email address"),
57+
externalId: z.string().optional().describe("External ID from your system for linking"),
58+
userMetadata: z.record(z.string(), z.string()).optional().describe("Custom metadata key-value pairs"),
3159
});
3260

33-
const DeleteCustomerInputSchema = z.object({ id: z.string() });
61+
export type CreateCustomerInput = z.infer<typeof CreateCustomerInputSchema>;
62+
export type UpdateCustomerInput = z.infer<typeof UpdateCustomerInputSchema>;
3463

3564
// SDK contract - uses flexible lookup (externalId/email/customerId)
3665
export const getSdkCustomerContract = oc
3766
.input(SdkGetCustomerInputSchema)
3867
.output(CustomerWithSubscriptionsSchema);
3968

40-
// MCP contracts
69+
// Contracts
4170
export const listCustomersContract = oc
42-
.input(ListCustomersInputSchema)
71+
.input(z.object({}))
4372
.output(ListCustomersOutputSchema);
4473

74+
export const listCustomersPaginatedContract = oc
75+
.input(ListCustomersPaginatedInputSchema)
76+
.output(ListCustomersPaginatedOutputSchema);
77+
4578
export const getCustomerContract = oc
46-
.input(McpGetCustomerInputSchema)
79+
.input(GetCustomerInputSchema)
4780
.output(CustomerSchema);
4881

4982
export const createCustomerContract = oc
@@ -56,10 +89,11 @@ export const updateCustomerContract = oc
5689

5790
export const deleteCustomerContract = oc
5891
.input(DeleteCustomerInputSchema)
59-
.output(z.object({ ok: z.literal(true) }));
92+
.output(z.void());
6093

6194
export const customer = {
6295
list: listCustomersContract,
96+
listPaginated: listCustomersPaginatedContract,
6397
get: getCustomerContract,
6498
getSdk: getSdkCustomerContract,
6599
create: createCustomerContract,

0 commit comments

Comments
 (0)