Repository navigation
refactor: add list/listPaginated pattern and reorganize schemas #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
372433b
refactor: add list/listPaginated pattern and reorganize schemas
martinsaposnic 73d0431
fix: biome formatting
martinsaposnic 65406b9
refactor(customer): use discriminated union for lookup schema
martinsaposnic 5c433b1
feat(customer): add CustomerLookupToolSchema for MCP tools
martinsaposnic 0d686c4
fix(pagination): add validation constraints to PaginatedInputSchema
martinsaposnic 62de449
fix: biome formatting
martinsaposnic 30bdcba
Stop using discriminated union. MCP can't handle it
martinsaposnic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,33 @@ | ||
| import { oc } from "@orpc/contract"; | ||
| import { z } from "zod"; | ||
| import { CheckoutSchema } from "../schemas/checkout"; | ||
| import { | ||
| type CheckoutDetail, | ||
| CheckoutDetailSchema, | ||
| type CheckoutListItem, | ||
| CheckoutListItemSchema, | ||
| CheckoutSchema, | ||
| type CheckoutStatus, | ||
| CheckoutStatusSchema, | ||
| type CheckoutType, | ||
| CheckoutTypeSchema, | ||
| } from "../schemas/checkout"; | ||
| import { CurrencySchema } from "../schemas/currency"; | ||
| import { CustomerSchema } from "../schemas/customer"; | ||
| import { | ||
| PaginationInputSchema, | ||
| PaginatedInputSchema, | ||
| PaginationOutputSchema, | ||
| } from "../schemas/pagination"; | ||
|
|
||
| // Re-export entity schemas for backwards compatibility | ||
| export { | ||
| CheckoutStatusSchema, | ||
| CheckoutTypeSchema, | ||
| CheckoutListItemSchema, | ||
| CheckoutDetailSchema, | ||
| }; | ||
| export type { CheckoutStatus, CheckoutType, CheckoutListItem, CheckoutDetail }; | ||
|
|
||
| /** | ||
| * Helper to treat empty strings as undefined (not provided). | ||
| * This allows clients to pass empty strings without validation errors. | ||
| */ | ||
| const emptyStringToUndefined = z | ||
| .string() | ||
|
|
@@ -23,22 +40,12 @@ const emailOrEmpty = z.string().email().optional().or(z.literal("")); | |
|
|
||
| /** | ||
| * Valid fields that can be required at checkout time. | ||
| * - Standard fields: 'email', 'name' (checked against customer.email/name) | ||
| * - Any other string is a custom field (checked against customer[field]) | ||
| * | ||
| * @example ['email'] - require email | ||
| * @example ['email', 'name'] - require both email and name | ||
| * @example ['email', 'company'] - require email and company | ||
| */ | ||
| export const CustomerFieldSchema = z.string().min(1); | ||
| export type CustomerField = string; | ||
|
|
||
| /** | ||
| * Customer data object for checkout. | ||
| * Flat structure - standard fields (name, email, externalId) plus any custom string fields. | ||
| * Empty strings are treated as undefined (not provided). | ||
| * | ||
| * @example { name: "John", email: "[email protected]", externalId: "user_123", company: "Acme" } | ||
| * Customer data object for checkout input. | ||
| */ | ||
| export const CustomerInputSchema = z | ||
| .object({ | ||
|
|
@@ -50,6 +57,7 @@ export const CustomerInputSchema = z | |
|
|
||
| export type CustomerInput = z.infer<typeof CustomerInputSchema>; | ||
|
|
||
| // Input schemas | ||
| export const CreateCheckoutInputSchema = z.object({ | ||
| nodeId: z.string(), | ||
| amount: z.number().optional(), | ||
|
|
@@ -58,33 +66,13 @@ export const CreateCheckoutInputSchema = z.object({ | |
| successUrl: z.string().optional(), | ||
| allowDiscountCodes: z.boolean().optional(), | ||
| metadata: z.record(z.string(), z.any()).optional(), | ||
| /** | ||
| * Customer data for this checkout. | ||
| */ | ||
| customer: CustomerInputSchema.optional(), | ||
| /** | ||
| * Array of customer fields to require at checkout. | ||
| * If a field is listed here and not provided, the checkout UI will prompt for it. | ||
| * @example ['email'] - require email | ||
| * @example ['email', 'name'] - require both | ||
| */ | ||
| requireCustomerData: z.array(CustomerFieldSchema).optional(), | ||
| }); | ||
|
|
||
| export const ConfirmCheckoutInputSchema = z.object({ | ||
| checkoutId: z.string(), | ||
| /** | ||
| * 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({ | ||
|
|
@@ -120,25 +108,64 @@ export const PaymentReceivedInputSchema = z.object({ | |
| ), | ||
| }); | ||
|
|
||
| export const GetCheckoutInputSchema = z.object({ id: z.string() }); | ||
| export const GetCheckoutInputSchema = z.object({ | ||
| id: z.string().describe("The checkout ID"), | ||
| }); | ||
| export type GetCheckoutInput = z.infer<typeof GetCheckoutInputSchema>; | ||
|
|
||
| export type CreateCheckout = z.infer<typeof CreateCheckoutInputSchema>; | ||
| export type ConfirmCheckout = z.infer<typeof ConfirmCheckoutInputSchema>; | ||
| export type RegisterInvoice = z.infer<typeof RegisterInvoiceInputSchema>; | ||
| export type PaymentReceived = z.infer<typeof PaymentReceivedInputSchema>; | ||
|
|
||
| // List output schemas | ||
| export const ListCheckoutsOutputSchema = z.object({ | ||
| checkouts: z.array(CheckoutSchema), | ||
| }); | ||
| export type ListCheckoutsOutput = z.infer<typeof ListCheckoutsOutputSchema>; | ||
|
|
||
| export const ListCheckoutsPaginatedInputSchema = PaginatedInputSchema.extend({ | ||
| status: CheckoutStatusSchema.optional().describe( | ||
| "Filter by status: UNCONFIRMED, CONFIRMED, PENDING_PAYMENT, PAYMENT_RECEIVED, or EXPIRED", | ||
| ), | ||
| }); | ||
| export type ListCheckoutsPaginatedInput = z.infer< | ||
| typeof ListCheckoutsPaginatedInputSchema | ||
| >; | ||
|
|
||
| export const ListCheckoutsPaginatedOutputSchema = PaginationOutputSchema.extend( | ||
| { | ||
| checkouts: z.array(CheckoutSchema), | ||
| }, | ||
| ); | ||
| export type ListCheckoutsPaginatedOutput = z.infer< | ||
| typeof ListCheckoutsPaginatedOutputSchema | ||
| >; | ||
|
|
||
| export const ListCheckoutsSummaryOutputSchema = PaginationOutputSchema.extend({ | ||
| checkouts: z.array(CheckoutListItemSchema), | ||
| }); | ||
| export type ListCheckoutsSummaryOutput = z.infer< | ||
| typeof ListCheckoutsSummaryOutputSchema | ||
| >; | ||
|
|
||
| // Contracts | ||
| export const createCheckoutContract = oc | ||
| .input(CreateCheckoutInputSchema) | ||
| .output(CheckoutSchema); | ||
|
|
||
| export const applyDiscountCodeContract = oc | ||
| .input(ApplyDiscountCodeInputSchema) | ||
| .output(CheckoutSchema); | ||
|
|
||
| export const confirmCheckoutContract = oc | ||
| .input(ConfirmCheckoutInputSchema) | ||
| .output(CheckoutSchema); | ||
|
|
||
| export const registerInvoiceContract = oc | ||
| .input(RegisterInvoiceInputSchema) | ||
| .output(CheckoutSchema); | ||
|
|
||
| export const getCheckoutContract = oc | ||
| .input(GetCheckoutInputSchema) | ||
| .output(CheckoutSchema); | ||
|
|
@@ -147,67 +174,19 @@ export const paymentReceivedContract = oc | |
| .input(PaymentReceivedInputSchema) | ||
| .output(z.object({ ok: z.boolean() })); | ||
|
|
||
| // List checkouts schemas | ||
| export const CheckoutStatusSchema = z.enum([ | ||
| "UNCONFIRMED", | ||
| "CONFIRMED", | ||
| "PENDING_PAYMENT", | ||
| "PAYMENT_RECEIVED", | ||
| "EXPIRED", | ||
| ]); | ||
| export type CheckoutStatus = z.infer<typeof CheckoutStatusSchema>; | ||
|
|
||
| export const CheckoutTypeSchema = z.enum(["PRODUCTS", "AMOUNT", "TOP_UP"]); | ||
| export type CheckoutType = z.infer<typeof CheckoutTypeSchema>; | ||
|
|
||
| const ListCheckoutsInputSchema = PaginationInputSchema.extend({ | ||
| status: CheckoutStatusSchema.optional(), | ||
| }); | ||
|
|
||
| const ListCheckoutsOutputSchema = PaginationOutputSchema.extend({ | ||
| checkouts: z.array(CheckoutSchema), | ||
| }); | ||
|
|
||
| export const listCheckoutsContract = oc | ||
| .input(ListCheckoutsInputSchema) | ||
| .input(z.object({})) | ||
| .output(ListCheckoutsOutputSchema); | ||
|
|
||
| const CheckoutCustomerSchema = CustomerSchema.nullable(); | ||
|
|
||
| // MCP-specific summary schema for list (simpler than full CheckoutSchema) | ||
| const CheckoutListItemSchema = z.object({ | ||
| id: z.string(), | ||
| status: CheckoutStatusSchema, | ||
| type: CheckoutTypeSchema, | ||
| currency: CurrencySchema, | ||
| totalAmount: z.number().nullable(), | ||
| customerId: z.string().nullable(), | ||
| customer: CheckoutCustomerSchema, | ||
| productId: z.string().nullable(), | ||
| organizationId: z.string(), | ||
| expiresAt: z.date(), | ||
| createdAt: z.date(), | ||
| modifiedAt: z.date().nullable(), | ||
| }); | ||
|
|
||
| // MCP-specific detailed schema for get (includes additional fields) | ||
| const CheckoutDetailSchema = CheckoutListItemSchema.extend({ | ||
| userMetadata: z.record(z.unknown()).nullable(), | ||
| successUrl: z.string().nullable(), | ||
| discountAmount: z.number().nullable(), | ||
| netAmount: z.number().nullable(), | ||
| taxAmount: z.number().nullable(), | ||
| }); | ||
|
|
||
| const ListCheckoutsSummaryOutputSchema = PaginationOutputSchema.extend({ | ||
| checkouts: z.array(CheckoutListItemSchema), | ||
| }); | ||
| export const listCheckoutsPaginatedContract = oc | ||
| .input(ListCheckoutsPaginatedInputSchema) | ||
| .output(ListCheckoutsPaginatedOutputSchema); | ||
|
|
||
| export const listCheckoutsSummaryContract = oc | ||
| .input(ListCheckoutsInputSchema) | ||
| export const listCheckoutsSummaryPaginatedContract = oc | ||
| .input(ListCheckoutsPaginatedInputSchema) | ||
| .output(ListCheckoutsSummaryOutputSchema); | ||
|
|
||
| export const getCheckoutSummaryContract = oc | ||
| export const getCheckoutDetailContract = oc | ||
| .input(GetCheckoutInputSchema) | ||
| .output(CheckoutDetailSchema); | ||
|
|
||
|
|
@@ -218,6 +197,8 @@ export const checkout = { | |
| registerInvoice: registerInvoiceContract, | ||
| paymentReceived: paymentReceivedContract, | ||
| list: listCheckoutsContract, | ||
| listSummary: listCheckoutsSummaryContract, | ||
| getSummary: getCheckoutSummaryContract, | ||
| listPaginated: listCheckoutsPaginatedContract, | ||
| // Original names preserved | ||
| listSummary: listCheckoutsSummaryPaginatedContract, | ||
| getSummary: getCheckoutDetailContract, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CustomerLookupInputSchemais documented as “exactly one identifier,” but the union of non‑strict objects will accept payloads with multiple identifiers (e.g.,{ id, email }) and silently strip extras. That can cause a caller to pass conflicting identifiers and still get a result based only on the first matching branch. If you need to enforce exclusivity, add.strict()and a refinement or use a discriminated union so multi‑field payloads are rejected.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check 5c433b1