From b1b9683c9d2db08e827b3b85494a5441b167e0a3 Mon Sep 17 00:00:00 2001 From: npslaney Date: Fri, 23 Jan 2026 13:26:47 -0500 Subject: [PATCH 01/14] feat(api-contract): add shared schemas for MCP API --- src/schemas/customer.ts | 20 +++++++ src/schemas/order.ts | 50 ++++++++++++++++++ src/schemas/pagination.ts | 22 ++++++++ src/schemas/product-price-input.ts | 84 ++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 src/schemas/customer.ts create mode 100644 src/schemas/order.ts create mode 100644 src/schemas/pagination.ts create mode 100644 src/schemas/product-price-input.ts diff --git a/src/schemas/customer.ts b/src/schemas/customer.ts new file mode 100644 index 0000000..5eb4dbc --- /dev/null +++ b/src/schemas/customer.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +/** + * Customer schema for MCP API responses. + * Represents a customer in the organization. + * Note: Uses modifiedAt to match Prisma schema naming. + */ +export const CustomerSchema = z.object({ + id: z.string(), + name: z.string().nullable(), + email: z.string().nullable(), + emailVerified: z.boolean(), + externalId: z.string().nullable(), + userMetadata: z.record(z.string(), z.any()).nullable(), + organizationId: z.string(), + createdAt: z.date(), + modifiedAt: z.date().nullable(), +}); + +export type Customer = z.infer; diff --git a/src/schemas/order.ts b/src/schemas/order.ts new file mode 100644 index 0000000..6f59905 --- /dev/null +++ b/src/schemas/order.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +/** + * Order status enum matching Prisma OrderStatus. + * Note: Prisma uses String type, so we validate against known values. + */ +export const OrderStatusSchema = z.enum([ + "PENDING", + "PAID", + "REFUNDED", + "CANCELLED", +]); + +export type OrderStatus = z.infer; + +/** + * Order item schema representing a line item in an order. + * Note: Uses modifiedAt to match Prisma schema naming. + */ +export const OrderItemSchema = z.object({ + id: z.string(), + orderId: z.string(), + productPriceId: z.string().nullable(), + label: z.string(), + amount: z.number(), + createdAt: z.date(), + modifiedAt: z.date().nullable(), +}); + +export type OrderItem = z.infer; + +/** + * Order schema for MCP API responses. + * Note: Uses modifiedAt to match Prisma schema naming. + * Note: Order doesn't have totalAmount directly - it's calculated from subtotalAmount + taxAmount. + */ +export const OrderSchema = z.object({ + id: z.string(), + organizationId: z.string(), + customerId: z.string().nullable(), + status: z.string(), // Prisma uses String, not enum + currency: z.string(), + subtotalAmount: z.number(), + taxAmount: z.number(), + userMetadata: z.record(z.string(), z.any()).nullable(), + createdAt: z.date(), + modifiedAt: z.date().nullable(), +}); + +export type Order = z.infer; diff --git a/src/schemas/pagination.ts b/src/schemas/pagination.ts new file mode 100644 index 0000000..f9b9a8d --- /dev/null +++ b/src/schemas/pagination.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +/** + * Pagination input schema for list operations. + * Uses cursor-based pagination for efficient large dataset traversal. + */ +export const PaginationInputSchema = z.object({ + limit: z.number().int().min(1).max(100).default(50), + cursor: z.string().optional(), +}); + +export type PaginationInput = z.infer; + +/** + * Pagination output schema for list operations. + * Returns a cursor for the next page, or null if no more results. + */ +export const PaginationOutputSchema = z.object({ + nextCursor: z.string().nullable(), +}); + +export type PaginationOutput = z.infer; diff --git a/src/schemas/product-price-input.ts b/src/schemas/product-price-input.ts new file mode 100644 index 0000000..9d21e47 --- /dev/null +++ b/src/schemas/product-price-input.ts @@ -0,0 +1,84 @@ +import { z } from "zod"; +import { CurrencySchema } from "./currency"; + +/** + * COPIED from moneydevkit.com/lib/products/schema.ts - ProductPriceFormSchema + * TODO: When api-contract moves to monorepo, import from shared location instead of copying + * + * This schema is used for MCP product create/update operations. + * It mirrors the dashboard's pricing validation logic. + */ + +// Price amount types +export const PriceAmountTypeSchema = z.enum(["FIXED", "CUSTOM"]); +export type PriceAmountType = z.infer; + +// Recurring interval with "never" option for one-time purchases +export const RecurringIntervalWithNeverSchema = z.enum([ + "NEVER", + "MONTH", + "QUARTER", + "YEAR", +]); +export type RecurringIntervalWithNever = z.infer< + typeof RecurringIntervalWithNeverSchema +>; + +/** + * Simplified pricing schema: one price per product. + * Validation rules vary by amountType: + * - FIXED: priceAmount required and positive + * - CUSTOM: minimumAmount and presetAmount optional (both non-negative if provided) + */ +export const ProductPriceInputSchema = z + .object({ + recurringInterval: RecurringIntervalWithNeverSchema, + currency: CurrencySchema, + amountType: PriceAmountTypeSchema, + // Required for FIXED, ignored for CUSTOM + priceAmount: z + .number() + .positive({ message: "Price must be greater than 0" }) + .optional(), + // Optional for CUSTOM: minimum amount customer can pay + minimumAmount: z + .number() + .nonnegative({ message: "Minimum amount cannot be negative" }) + .optional(), + // Optional for CUSTOM: suggested/default amount + presetAmount: z + .number() + .nonnegative({ message: "Preset amount cannot be negative" }) + .optional(), + }) + .superRefine((data, ctx) => { + if (data.amountType === "FIXED") { + if ( + data.priceAmount === undefined || + data.priceAmount === null || + isNaN(data.priceAmount) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Price must be set for fixed price products", + path: ["priceAmount"], + }); + } + } + // For CUSTOM: if both minimumAmount and presetAmount are set, preset should be >= minimum + if ( + data.amountType === "CUSTOM" && + data.minimumAmount !== undefined && + data.presetAmount !== undefined + ) { + if (data.presetAmount < data.minimumAmount) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Preset amount must be at least the minimum amount", + path: ["presetAmount"], + }); + } + } + }); + +export type ProductPriceInput = z.infer; From d8d1d50076098b7b58571fe0d89e4253f64c0c75 Mon Sep 17 00:00:00 2001 From: npslaney Date: Fri, 23 Jan 2026 13:29:03 -0500 Subject: [PATCH 02/14] feat(api-contract): add MCP contracts for CRUD operations Add separate mcpContract namespace for MCP tools: - customers: list, get, create, update, delete - products: list, get, create, update, delete - orders: list, get (read-only) - checkouts: list, get (read-only) Export mcpContract from index.ts alongside SDK contract. Co-Authored-By: Claude Opus 4.5 --- src/contracts/mcp/checkouts.ts | 68 ++++++++++++++++++++++++++++++++++ src/contracts/mcp/customers.ts | 56 ++++++++++++++++++++++++++++ src/contracts/mcp/index.ts | 15 ++++++++ src/contracts/mcp/orders.ts | 39 +++++++++++++++++++ src/contracts/mcp/products.ts | 66 +++++++++++++++++++++++++++++++++ src/index.ts | 29 +++++++++++++++ 6 files changed, 273 insertions(+) create mode 100644 src/contracts/mcp/checkouts.ts create mode 100644 src/contracts/mcp/customers.ts create mode 100644 src/contracts/mcp/index.ts create mode 100644 src/contracts/mcp/orders.ts create mode 100644 src/contracts/mcp/products.ts diff --git a/src/contracts/mcp/checkouts.ts b/src/contracts/mcp/checkouts.ts new file mode 100644 index 0000000..4591bd9 --- /dev/null +++ b/src/contracts/mcp/checkouts.ts @@ -0,0 +1,68 @@ +import { oc } from "@orpc/contract"; +import { z } from "zod"; +import { + PaginationInputSchema, + PaginationOutputSchema, +} from "../../schemas/pagination"; +import { CustomerSchema } from "../../schemas/customer"; +import { CurrencySchema } from "../../schemas/currency"; + +/** + * Checkout status enum. + * NOTE: This is "checkouts.*" namespace for read APIs (list/get), + * distinct from "checkout.*" namespace which handles the SDK create/confirm flow. + */ +const CheckoutStatusSchema = z.enum([ + "UNCONFIRMED", + "CONFIRMED", + "PENDING_PAYMENT", + "PAYMENT_RECEIVED", + "EXPIRED", +]); + +// Simplified checkout schema for MCP listing +// Note: Uses modifiedAt to match Prisma schema naming +const CheckoutListItemSchema = z.object({ + id: z.string(), + status: CheckoutStatusSchema, + type: z.enum(["PRODUCTS", "AMOUNT", "TOP_UP"]), + currency: CurrencySchema, + totalAmount: z.number().nullable(), + customerId: z.string().nullable(), + customer: CustomerSchema.nullable(), + productId: z.string().nullable(), + organizationId: z.string(), + expiresAt: z.date(), + createdAt: z.date(), + modifiedAt: z.date().nullable(), +}); + +// Full checkout detail schema +const CheckoutDetailSchema = CheckoutListItemSchema.extend({ + userMetadata: z.record(z.string(), z.any()).nullable(), + successUrl: z.string().nullable(), + discountAmount: z.number().nullable(), + netAmount: z.number().nullable(), + taxAmount: z.number().nullable(), +}); + +const ListCheckoutsInputSchema = PaginationInputSchema.extend({ + status: CheckoutStatusSchema.optional(), +}); + +const ListCheckoutsOutputSchema = PaginationOutputSchema.extend({ + checkouts: z.array(CheckoutListItemSchema), +}); + +export const listCheckoutsContract = oc + .input(ListCheckoutsInputSchema) + .output(ListCheckoutsOutputSchema); + +export const getCheckoutContract = oc + .input(z.object({ id: z.string() })) + .output(CheckoutDetailSchema); + +export const checkouts = { + list: listCheckoutsContract, + get: getCheckoutContract, +}; diff --git a/src/contracts/mcp/customers.ts b/src/contracts/mcp/customers.ts new file mode 100644 index 0000000..1b6f97e --- /dev/null +++ b/src/contracts/mcp/customers.ts @@ -0,0 +1,56 @@ +import { oc } from "@orpc/contract"; +import { z } from "zod"; +import { CustomerSchema } from "../../schemas/customer"; +import { + PaginationInputSchema, + PaginationOutputSchema, +} from "../../schemas/pagination"; + +const ListCustomersInputSchema = PaginationInputSchema; +const ListCustomersOutputSchema = PaginationOutputSchema.extend({ + customers: z.array(CustomerSchema), +}); + +const GetCustomerInputSchema = z.object({ id: z.string() }); + +const CreateCustomerInputSchema = z.object({ + name: z.string().min(1), + email: z.string().email(), +}); + +const UpdateCustomerInputSchema = z.object({ + id: z.string(), + name: z.string().optional(), + email: z.string().email().optional(), + userMetadata: z.record(z.string(), z.string()).optional(), +}); + +const DeleteCustomerInputSchema = z.object({ id: z.string() }); + +export const listCustomersContract = oc + .input(ListCustomersInputSchema) + .output(ListCustomersOutputSchema); + +export const getCustomerContract = oc + .input(GetCustomerInputSchema) + .output(CustomerSchema); + +export const createCustomerContract = oc + .input(CreateCustomerInputSchema) + .output(CustomerSchema); + +export const updateCustomerContract = oc + .input(UpdateCustomerInputSchema) + .output(CustomerSchema); + +export const deleteCustomerContract = oc + .input(DeleteCustomerInputSchema) + .output(z.object({ ok: z.literal(true) })); + +export const customers = { + list: listCustomersContract, + get: getCustomerContract, + create: createCustomerContract, + update: updateCustomerContract, + delete: deleteCustomerContract, +}; diff --git a/src/contracts/mcp/index.ts b/src/contracts/mcp/index.ts new file mode 100644 index 0000000..2346cb5 --- /dev/null +++ b/src/contracts/mcp/index.ts @@ -0,0 +1,15 @@ +/** + * MCP Contract namespace - separate from SDK contract. + * + * This contract is used by MCP tools for organization management. + * It uses OAuth authentication (not API key auth) and provides + * CRUD operations for customers, products, orders, and checkouts. + * + * NOTE: This is deliberately NOT exported to the SDK. It's only + * consumed by the MCP server via the dedicated /rpc/mcp endpoint. + */ + +export { customers } from "./customers"; +export { products } from "./products"; +export { orders } from "./orders"; +export { checkouts } from "./checkouts"; diff --git a/src/contracts/mcp/orders.ts b/src/contracts/mcp/orders.ts new file mode 100644 index 0000000..056ee9f --- /dev/null +++ b/src/contracts/mcp/orders.ts @@ -0,0 +1,39 @@ +import { oc } from "@orpc/contract"; +import { z } from "zod"; +import { OrderSchema, OrderItemSchema } from "../../schemas/order"; +import { + PaginationInputSchema, + PaginationOutputSchema, +} from "../../schemas/pagination"; +import { CustomerSchema } from "../../schemas/customer"; + +// Order with related data for list view +const OrderWithRelationsSchema = OrderSchema.extend({ + customer: CustomerSchema.nullable(), + orderItems: z.array(OrderItemSchema), +}); + +// Order with full details for get view +const OrderDetailSchema = OrderWithRelationsSchema; + +const ListOrdersInputSchema = PaginationInputSchema.extend({ + customerId: z.string().optional(), + status: z.string().optional(), // Prisma uses String type for status +}); + +const ListOrdersOutputSchema = PaginationOutputSchema.extend({ + orders: z.array(OrderWithRelationsSchema), +}); + +export const listOrdersContract = oc + .input(ListOrdersInputSchema) + .output(ListOrdersOutputSchema); + +export const getOrderContract = oc + .input(z.object({ id: z.string() })) + .output(OrderDetailSchema); + +export const orders = { + list: listOrdersContract, + get: getOrderContract, +}; diff --git a/src/contracts/mcp/products.ts b/src/contracts/mcp/products.ts new file mode 100644 index 0000000..1e52298 --- /dev/null +++ b/src/contracts/mcp/products.ts @@ -0,0 +1,66 @@ +import { oc } from "@orpc/contract"; +import { z } from "zod"; +import { ProductSchema, ProductPriceSchema } from "../products"; +import { + PaginationInputSchema, + PaginationOutputSchema, +} from "../../schemas/pagination"; +import { ProductPriceInputSchema } from "../../schemas/product-price-input"; + +// Output schema - product with its active price +// Note: Uses modifiedAt to match Prisma schema naming +const ProductWithPriceSchema = ProductSchema.omit({ prices: true }).extend({ + price: ProductPriceSchema.nullable(), + userMetadata: z.record(z.string(), z.any()).nullable(), + createdAt: z.date(), + modifiedAt: z.date().nullable(), +}); + +const ListProductsOutputSchema = PaginationOutputSchema.extend({ + products: z.array(ProductWithPriceSchema), +}); + +// Create input - NO benefitIds (not exposed on dashboard MCP) +const CreateProductInputSchema = z.object({ + name: z.string().min(1), + description: z.string().optional(), + price: ProductPriceInputSchema, + userMetadata: z.record(z.string(), z.string()).optional(), +}); + +// Update input - includes price (matches dashboard updateProduct), NO benefitIds +const UpdateProductInputSchema = z.object({ + id: z.string(), + name: z.string().min(1).optional(), + description: z.string().optional(), + price: ProductPriceInputSchema.optional(), // Can update pricing (immutable pattern applies) + userMetadata: z.record(z.string(), z.string()).optional(), +}); + +export const listProductsContract = oc + .input(PaginationInputSchema) + .output(ListProductsOutputSchema); + +export const getProductContract = oc + .input(z.object({ id: z.string() })) + .output(ProductWithPriceSchema); + +export const createProductContract = oc + .input(CreateProductInputSchema) + .output(ProductWithPriceSchema); + +export const updateProductContract = oc + .input(UpdateProductInputSchema) + .output(ProductWithPriceSchema); + +export const deleteProductContract = oc + .input(z.object({ id: z.string() })) + .output(z.object({ ok: z.literal(true) })); + +export const products = { + list: listProductsContract, + get: getProductContract, + create: createProductContract, + update: updateProductContract, + delete: deleteProductContract, +}; diff --git a/src/index.ts b/src/index.ts index affa6d5..c784019 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,12 @@ import { checkout } from "./contracts/checkout"; import { onboarding } from "./contracts/onboarding"; import { products } from "./contracts/products"; +import { + customers as mcpCustomers, + products as mcpProducts, + orders as mcpOrders, + checkouts as mcpCheckouts, +} from "./contracts/mcp"; export type { ConfirmCheckout, @@ -28,8 +34,31 @@ export { ListProductsOutputSchema, } from "./contracts/products"; +// New MCP schemas +export type { Customer } from "./schemas/customer"; +export { CustomerSchema } from "./schemas/customer"; +export type { Order, OrderItem } from "./schemas/order"; +export { OrderSchema, OrderItemSchema } from "./schemas/order"; +export type { PaginationInput, PaginationOutput } from "./schemas/pagination"; +export { PaginationInputSchema, PaginationOutputSchema } from "./schemas/pagination"; +export type { ProductPriceInput } from "./schemas/product-price-input"; +export { ProductPriceInputSchema } from "./schemas/product-price-input"; + +// SDK contract - consumed by SDK clients export const contract = { checkout, onboarding, products }; +/** + * MCP contract - separate namespace for MCP tools. + * NOT consumed by SDK, only by MCP server via /rpc/mcp endpoint. + * Uses OAuth authentication (not API key auth). + */ +export const mcpContract = { + customers: mcpCustomers, + products: mcpProducts, + orders: mcpOrders, + checkouts: mcpCheckouts, +}; + export type { MetadataValidationError } from "./validation/metadata-validation"; export { MAX_KEY_COUNT, From 2aededc0294a151a4ceca02fac4456d30e35091c Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 11:49:56 -0500 Subject: [PATCH 03/14] fix: address biome linting issues - Organize imports alphabetically - Use Number.isNaN instead of global isNaN - Format multi-line exports Co-Authored-By: Claude Opus 4.5 --- src/contracts/mcp/checkouts.ts | 4 ++-- src/contracts/mcp/orders.ts | 4 ++-- src/contracts/mcp/products.ts | 2 +- src/index.ts | 13 ++++++++----- src/schemas/product-price-input.ts | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/contracts/mcp/checkouts.ts b/src/contracts/mcp/checkouts.ts index 4591bd9..e0f7724 100644 --- a/src/contracts/mcp/checkouts.ts +++ b/src/contracts/mcp/checkouts.ts @@ -1,11 +1,11 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; +import { CurrencySchema } from "../../schemas/currency"; +import { CustomerSchema } from "../../schemas/customer"; import { PaginationInputSchema, PaginationOutputSchema, } from "../../schemas/pagination"; -import { CustomerSchema } from "../../schemas/customer"; -import { CurrencySchema } from "../../schemas/currency"; /** * Checkout status enum. diff --git a/src/contracts/mcp/orders.ts b/src/contracts/mcp/orders.ts index 056ee9f..a514ab6 100644 --- a/src/contracts/mcp/orders.ts +++ b/src/contracts/mcp/orders.ts @@ -1,11 +1,11 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; -import { OrderSchema, OrderItemSchema } from "../../schemas/order"; +import { CustomerSchema } from "../../schemas/customer"; +import { OrderItemSchema, OrderSchema } from "../../schemas/order"; import { PaginationInputSchema, PaginationOutputSchema, } from "../../schemas/pagination"; -import { CustomerSchema } from "../../schemas/customer"; // Order with related data for list view const OrderWithRelationsSchema = OrderSchema.extend({ diff --git a/src/contracts/mcp/products.ts b/src/contracts/mcp/products.ts index 1e52298..648ec72 100644 --- a/src/contracts/mcp/products.ts +++ b/src/contracts/mcp/products.ts @@ -1,11 +1,11 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; -import { ProductSchema, ProductPriceSchema } from "../products"; import { PaginationInputSchema, PaginationOutputSchema, } from "../../schemas/pagination"; import { ProductPriceInputSchema } from "../../schemas/product-price-input"; +import { ProductPriceSchema, ProductSchema } from "../products"; // Output schema - product with its active price // Note: Uses modifiedAt to match Prisma schema naming diff --git a/src/index.ts b/src/index.ts index c784019..b6e4b8e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ import { checkout } from "./contracts/checkout"; -import { onboarding } from "./contracts/onboarding"; -import { products } from "./contracts/products"; import { + checkouts as mcpCheckouts, customers as mcpCustomers, - products as mcpProducts, orders as mcpOrders, - checkouts as mcpCheckouts, + products as mcpProducts, } from "./contracts/mcp"; +import { onboarding } from "./contracts/onboarding"; +import { products } from "./contracts/products"; export type { ConfirmCheckout, @@ -40,7 +40,10 @@ export { CustomerSchema } from "./schemas/customer"; export type { Order, OrderItem } from "./schemas/order"; export { OrderSchema, OrderItemSchema } from "./schemas/order"; export type { PaginationInput, PaginationOutput } from "./schemas/pagination"; -export { PaginationInputSchema, PaginationOutputSchema } from "./schemas/pagination"; +export { + PaginationInputSchema, + PaginationOutputSchema, +} from "./schemas/pagination"; export type { ProductPriceInput } from "./schemas/product-price-input"; export { ProductPriceInputSchema } from "./schemas/product-price-input"; diff --git a/src/schemas/product-price-input.ts b/src/schemas/product-price-input.ts index 9d21e47..ef71e8e 100644 --- a/src/schemas/product-price-input.ts +++ b/src/schemas/product-price-input.ts @@ -56,7 +56,7 @@ export const ProductPriceInputSchema = z if ( data.priceAmount === undefined || data.priceAmount === null || - isNaN(data.priceAmount) + Number.isNaN(data.priceAmount) ) { ctx.addIssue({ code: z.ZodIssueCode.custom, From 57b490d56321d6ce441f957bf2908060ce59289c Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 15:30:15 -0500 Subject: [PATCH 04/14] fix(api-contract): use typed schemas for OrderSchema status and currency - Use OrderStatusSchema instead of z.string() for status field - Use CurrencySchema instead of z.string() for currency field - Export OrderStatusSchema and OrderStatus type for consumers This ensures the contract validates against known enum values rather than accepting arbitrary strings, preventing silent acceptance of invalid values. Co-Authored-By: Claude Opus 4.5 --- src/index.ts | 4 ++-- src/schemas/order.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index b6e4b8e..a54b4fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,8 +37,8 @@ export { // New MCP schemas export type { Customer } from "./schemas/customer"; export { CustomerSchema } from "./schemas/customer"; -export type { Order, OrderItem } from "./schemas/order"; -export { OrderSchema, OrderItemSchema } from "./schemas/order"; +export type { Order, OrderItem, OrderStatus } from "./schemas/order"; +export { OrderSchema, OrderItemSchema, OrderStatusSchema } from "./schemas/order"; export type { PaginationInput, PaginationOutput } from "./schemas/pagination"; export { PaginationInputSchema, diff --git a/src/schemas/order.ts b/src/schemas/order.ts index 6f59905..cf4cd2b 100644 --- a/src/schemas/order.ts +++ b/src/schemas/order.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { CurrencySchema } from "./currency"; /** * Order status enum matching Prisma OrderStatus. @@ -38,8 +39,8 @@ export const OrderSchema = z.object({ id: z.string(), organizationId: z.string(), customerId: z.string().nullable(), - status: z.string(), // Prisma uses String, not enum - currency: z.string(), + status: OrderStatusSchema, + currency: CurrencySchema, subtotalAmount: z.number(), taxAmount: z.number(), userMetadata: z.record(z.string(), z.any()).nullable(), From da5159882ded924d074c180bed01497eea2deac1 Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 15:31:10 -0500 Subject: [PATCH 05/14] fix: format export statement per biome rules Co-Authored-By: Claude Opus 4.5 --- src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index a54b4fe..0854e26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,7 +38,11 @@ export { export type { Customer } from "./schemas/customer"; export { CustomerSchema } from "./schemas/customer"; export type { Order, OrderItem, OrderStatus } from "./schemas/order"; -export { OrderSchema, OrderItemSchema, OrderStatusSchema } from "./schemas/order"; +export { + OrderSchema, + OrderItemSchema, + OrderStatusSchema, +} from "./schemas/order"; export type { PaginationInput, PaginationOutput } from "./schemas/pagination"; export { PaginationInputSchema, From 6d0069825f4685c42702d08c37cc6dbc024e9faa Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 15:47:56 -0500 Subject: [PATCH 06/14] =?UTF-8?q?fix(api-contract):=20document=20NEVER?= =?UTF-8?q?=E2=86=92null=20normalization=20for=20recurringInterval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename RecurringIntervalWithNeverSchema → RecurringIntervalInputSchema - Export RecurringIntervalInputSchema and RecurringIntervalInput type - Document that server normalizes "NEVER" to null when storing/returning Input accepts "NEVER" for one-time products, output uses null. Server-side normalization ensures contract consistency. Co-Authored-By: Claude Opus 4.5 --- src/index.ts | 10 ++++++++-- src/schemas/product-price-input.ts | 14 +++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0854e26..f7f4e89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,8 +48,14 @@ export { PaginationInputSchema, PaginationOutputSchema, } from "./schemas/pagination"; -export type { ProductPriceInput } from "./schemas/product-price-input"; -export { ProductPriceInputSchema } from "./schemas/product-price-input"; +export type { + ProductPriceInput, + RecurringIntervalInput, +} from "./schemas/product-price-input"; +export { + ProductPriceInputSchema, + RecurringIntervalInputSchema, +} from "./schemas/product-price-input"; // SDK contract - consumed by SDK clients export const contract = { checkout, onboarding, products }; diff --git a/src/schemas/product-price-input.ts b/src/schemas/product-price-input.ts index ef71e8e..e4adda0 100644 --- a/src/schemas/product-price-input.ts +++ b/src/schemas/product-price-input.ts @@ -13,15 +13,19 @@ import { CurrencySchema } from "./currency"; export const PriceAmountTypeSchema = z.enum(["FIXED", "CUSTOM"]); export type PriceAmountType = z.infer; -// Recurring interval with "never" option for one-time purchases -export const RecurringIntervalWithNeverSchema = z.enum([ +/** + * Recurring interval schema for product INPUT (MCP create/update). + * Uses "NEVER" explicitly for one-time purchases. + * Server normalizes "NEVER" → null when storing/returning. + */ +export const RecurringIntervalInputSchema = z.enum([ "NEVER", "MONTH", "QUARTER", "YEAR", ]); -export type RecurringIntervalWithNever = z.infer< - typeof RecurringIntervalWithNeverSchema +export type RecurringIntervalInput = z.infer< + typeof RecurringIntervalInputSchema >; /** @@ -32,7 +36,7 @@ export type RecurringIntervalWithNever = z.infer< */ export const ProductPriceInputSchema = z .object({ - recurringInterval: RecurringIntervalWithNeverSchema, + recurringInterval: RecurringIntervalInputSchema, currency: CurrencySchema, amountType: PriceAmountTypeSchema, // Required for FIXED, ignored for CUSTOM From 37216cc291cde9fc44f44afc62025b60e1404c8c Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 16:40:49 -0500 Subject: [PATCH 07/14] feat(api-contract): add list method to checkout contract Add pagination-based list endpoint for checkouts with optional status filter. This merges the MCP checkouts.list functionality into the unified checkout contract. Co-Authored-By: Claude Opus 4.5 --- src/contracts/checkout.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/contracts/checkout.ts b/src/contracts/checkout.ts index 2c6b848..7fbeff0 100644 --- a/src/contracts/checkout.ts +++ b/src/contracts/checkout.ts @@ -2,6 +2,10 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; import { CheckoutSchema } from "../schemas/checkout"; import { CurrencySchema } from "../schemas/currency"; +import { + PaginationInputSchema, + PaginationOutputSchema, +} from "../schemas/pagination"; /** * Helper to treat empty strings as undefined (not provided). @@ -142,10 +146,32 @@ export const paymentReceivedContract = oc .input(PaymentReceivedInputSchema) .output(z.object({ ok: z.boolean() })); +// List checkouts schemas +const CheckoutStatusSchema = z.enum([ + "UNCONFIRMED", + "CONFIRMED", + "PENDING_PAYMENT", + "PAYMENT_RECEIVED", + "EXPIRED", +]); + +const ListCheckoutsInputSchema = PaginationInputSchema.extend({ + status: CheckoutStatusSchema.optional(), +}); + +const ListCheckoutsOutputSchema = PaginationOutputSchema.extend({ + checkouts: z.array(CheckoutSchema), +}); + +export const listCheckoutsContract = oc + .input(ListCheckoutsInputSchema) + .output(ListCheckoutsOutputSchema); + export const checkout = { get: getCheckoutContract, create: createCheckoutContract, confirm: confirmCheckoutContract, registerInvoice: registerInvoiceContract, paymentReceived: paymentReceivedContract, + list: listCheckoutsContract, }; From cc0b6da54265d0c71ebc382a0a843a7a8b3a1628 Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 16:41:36 -0500 Subject: [PATCH 08/14] feat(api-contract): add CRUD methods to products contract Add get, create, update, delete endpoints to products contract. Keep existing list endpoint unchanged (no pagination). Co-Authored-By: Claude Opus 4.5 --- src/contracts/products.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/contracts/products.ts b/src/contracts/products.ts index faba3ea..d25359e 100644 --- a/src/contracts/products.ts +++ b/src/contracts/products.ts @@ -1,6 +1,7 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; import { CurrencySchema } from "../schemas/currency"; +import { ProductPriceInputSchema } from "../schemas/product-price-input"; export const ProductPriceSchema = z.object({ id: z.string(), @@ -31,6 +32,42 @@ export const listProductsContract = oc .input(z.object({}).optional()) .output(ListProductsOutputSchema); +// CRUD input schemas +const CreateProductInputSchema = z.object({ + name: z.string().min(1), + description: z.string().optional(), + price: ProductPriceInputSchema, + userMetadata: z.record(z.string(), z.string()).optional(), +}); + +const UpdateProductInputSchema = z.object({ + id: z.string(), + name: z.string().min(1).optional(), + description: z.string().optional(), + price: ProductPriceInputSchema.optional(), + userMetadata: z.record(z.string(), z.string()).optional(), +}); + +export const getProductContract = oc + .input(z.object({ id: z.string() })) + .output(ProductSchema); + +export const createProductContract = oc + .input(CreateProductInputSchema) + .output(ProductSchema); + +export const updateProductContract = oc + .input(UpdateProductInputSchema) + .output(ProductSchema); + +export const deleteProductContract = oc + .input(z.object({ id: z.string() })) + .output(z.object({ ok: z.literal(true) })); + export const products = { list: listProductsContract, + get: getProductContract, + create: createProductContract, + update: updateProductContract, + delete: deleteProductContract, }; From 01db3919f91d48094817c1c033b44b21f1a85417 Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 16:42:19 -0500 Subject: [PATCH 09/14] feat(api-contract): add customer contract Add customer contract with CRUD operations (list, get, create, update, delete). Uses singular naming convention. Co-Authored-By: Claude Opus 4.5 --- src/contracts/customer.ts | 56 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/contracts/customer.ts diff --git a/src/contracts/customer.ts b/src/contracts/customer.ts new file mode 100644 index 0000000..699edfc --- /dev/null +++ b/src/contracts/customer.ts @@ -0,0 +1,56 @@ +import { oc } from "@orpc/contract"; +import { z } from "zod"; +import { CustomerSchema } from "../schemas/customer"; +import { + PaginationInputSchema, + PaginationOutputSchema, +} from "../schemas/pagination"; + +const ListCustomersInputSchema = PaginationInputSchema; +const ListCustomersOutputSchema = PaginationOutputSchema.extend({ + customers: z.array(CustomerSchema), +}); + +const GetCustomerInputSchema = z.object({ id: z.string() }); + +const CreateCustomerInputSchema = z.object({ + name: z.string().min(1), + email: z.string().email(), +}); + +const UpdateCustomerInputSchema = z.object({ + id: z.string(), + name: z.string().optional(), + email: z.string().email().optional(), + userMetadata: z.record(z.string(), z.string()).optional(), +}); + +const DeleteCustomerInputSchema = z.object({ id: z.string() }); + +export const listCustomersContract = oc + .input(ListCustomersInputSchema) + .output(ListCustomersOutputSchema); + +export const getCustomerContract = oc + .input(GetCustomerInputSchema) + .output(CustomerSchema); + +export const createCustomerContract = oc + .input(CreateCustomerInputSchema) + .output(CustomerSchema); + +export const updateCustomerContract = oc + .input(UpdateCustomerInputSchema) + .output(CustomerSchema); + +export const deleteCustomerContract = oc + .input(DeleteCustomerInputSchema) + .output(z.object({ ok: z.literal(true) })); + +export const customer = { + list: listCustomersContract, + get: getCustomerContract, + create: createCustomerContract, + update: updateCustomerContract, + delete: deleteCustomerContract, +}; diff --git a/src/index.ts b/src/index.ts index f7f4e89..18b020d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { checkout } from "./contracts/checkout"; +import { customer } from "./contracts/customer"; import { checkouts as mcpCheckouts, customers as mcpCustomers, @@ -58,7 +59,7 @@ export { } from "./schemas/product-price-input"; // SDK contract - consumed by SDK clients -export const contract = { checkout, onboarding, products }; +export const contract = { checkout, customer, onboarding, products }; /** * MCP contract - separate namespace for MCP tools. From 05c871c87061fb5be3cab3dece3a266b6245c402 Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 16:43:02 -0500 Subject: [PATCH 10/14] feat(api-contract): add order contract Add order contract with list and get operations. Uses singular naming convention. Co-Authored-By: Claude Opus 4.5 --- src/contracts/order.ts | 39 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/contracts/order.ts diff --git a/src/contracts/order.ts b/src/contracts/order.ts new file mode 100644 index 0000000..065ce00 --- /dev/null +++ b/src/contracts/order.ts @@ -0,0 +1,39 @@ +import { oc } from "@orpc/contract"; +import { z } from "zod"; +import { CustomerSchema } from "../schemas/customer"; +import { OrderItemSchema, OrderSchema } from "../schemas/order"; +import { + PaginationInputSchema, + PaginationOutputSchema, +} from "../schemas/pagination"; + +// Order with related data for list view +const OrderWithRelationsSchema = OrderSchema.extend({ + customer: CustomerSchema.nullable(), + orderItems: z.array(OrderItemSchema), +}); + +// Order with full details for get view +const OrderDetailSchema = OrderWithRelationsSchema; + +const ListOrdersInputSchema = PaginationInputSchema.extend({ + customerId: z.string().optional(), + status: z.string().optional(), // Prisma uses String type for status +}); + +const ListOrdersOutputSchema = PaginationOutputSchema.extend({ + orders: z.array(OrderWithRelationsSchema), +}); + +export const listOrdersContract = oc + .input(ListOrdersInputSchema) + .output(ListOrdersOutputSchema); + +export const getOrderContract = oc + .input(z.object({ id: z.string() })) + .output(OrderDetailSchema); + +export const order = { + list: listOrdersContract, + get: getOrderContract, +}; diff --git a/src/index.ts b/src/index.ts index 18b020d..18f6d8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { products as mcpProducts, } from "./contracts/mcp"; import { onboarding } from "./contracts/onboarding"; +import { order } from "./contracts/order"; import { products } from "./contracts/products"; export type { @@ -59,7 +60,7 @@ export { } from "./schemas/product-price-input"; // SDK contract - consumed by SDK clients -export const contract = { checkout, customer, onboarding, products }; +export const contract = { checkout, customer, onboarding, order, products }; /** * MCP contract - separate namespace for MCP tools. From 1653e906f45d1df82fa7393d8ebe836d94c93a36 Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 16:43:42 -0500 Subject: [PATCH 11/14] refactor(api-contract): remove mcpContract, use unified contract Remove the separate mcp/ directory and mcpContract export. All contracts are now in the unified contract object. Co-Authored-By: Claude Opus 4.5 --- src/contracts/mcp/checkouts.ts | 68 ---------------------------------- src/contracts/mcp/customers.ts | 56 ---------------------------- src/contracts/mcp/index.ts | 15 -------- src/contracts/mcp/orders.ts | 39 ------------------- src/contracts/mcp/products.ts | 66 --------------------------------- src/index.ts | 20 +--------- 6 files changed, 1 insertion(+), 263 deletions(-) delete mode 100644 src/contracts/mcp/checkouts.ts delete mode 100644 src/contracts/mcp/customers.ts delete mode 100644 src/contracts/mcp/index.ts delete mode 100644 src/contracts/mcp/orders.ts delete mode 100644 src/contracts/mcp/products.ts diff --git a/src/contracts/mcp/checkouts.ts b/src/contracts/mcp/checkouts.ts deleted file mode 100644 index e0f7724..0000000 --- a/src/contracts/mcp/checkouts.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { oc } from "@orpc/contract"; -import { z } from "zod"; -import { CurrencySchema } from "../../schemas/currency"; -import { CustomerSchema } from "../../schemas/customer"; -import { - PaginationInputSchema, - PaginationOutputSchema, -} from "../../schemas/pagination"; - -/** - * Checkout status enum. - * NOTE: This is "checkouts.*" namespace for read APIs (list/get), - * distinct from "checkout.*" namespace which handles the SDK create/confirm flow. - */ -const CheckoutStatusSchema = z.enum([ - "UNCONFIRMED", - "CONFIRMED", - "PENDING_PAYMENT", - "PAYMENT_RECEIVED", - "EXPIRED", -]); - -// Simplified checkout schema for MCP listing -// Note: Uses modifiedAt to match Prisma schema naming -const CheckoutListItemSchema = z.object({ - id: z.string(), - status: CheckoutStatusSchema, - type: z.enum(["PRODUCTS", "AMOUNT", "TOP_UP"]), - currency: CurrencySchema, - totalAmount: z.number().nullable(), - customerId: z.string().nullable(), - customer: CustomerSchema.nullable(), - productId: z.string().nullable(), - organizationId: z.string(), - expiresAt: z.date(), - createdAt: z.date(), - modifiedAt: z.date().nullable(), -}); - -// Full checkout detail schema -const CheckoutDetailSchema = CheckoutListItemSchema.extend({ - userMetadata: z.record(z.string(), z.any()).nullable(), - successUrl: z.string().nullable(), - discountAmount: z.number().nullable(), - netAmount: z.number().nullable(), - taxAmount: z.number().nullable(), -}); - -const ListCheckoutsInputSchema = PaginationInputSchema.extend({ - status: CheckoutStatusSchema.optional(), -}); - -const ListCheckoutsOutputSchema = PaginationOutputSchema.extend({ - checkouts: z.array(CheckoutListItemSchema), -}); - -export const listCheckoutsContract = oc - .input(ListCheckoutsInputSchema) - .output(ListCheckoutsOutputSchema); - -export const getCheckoutContract = oc - .input(z.object({ id: z.string() })) - .output(CheckoutDetailSchema); - -export const checkouts = { - list: listCheckoutsContract, - get: getCheckoutContract, -}; diff --git a/src/contracts/mcp/customers.ts b/src/contracts/mcp/customers.ts deleted file mode 100644 index 1b6f97e..0000000 --- a/src/contracts/mcp/customers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { oc } from "@orpc/contract"; -import { z } from "zod"; -import { CustomerSchema } from "../../schemas/customer"; -import { - PaginationInputSchema, - PaginationOutputSchema, -} from "../../schemas/pagination"; - -const ListCustomersInputSchema = PaginationInputSchema; -const ListCustomersOutputSchema = PaginationOutputSchema.extend({ - customers: z.array(CustomerSchema), -}); - -const GetCustomerInputSchema = z.object({ id: z.string() }); - -const CreateCustomerInputSchema = z.object({ - name: z.string().min(1), - email: z.string().email(), -}); - -const UpdateCustomerInputSchema = z.object({ - id: z.string(), - name: z.string().optional(), - email: z.string().email().optional(), - userMetadata: z.record(z.string(), z.string()).optional(), -}); - -const DeleteCustomerInputSchema = z.object({ id: z.string() }); - -export const listCustomersContract = oc - .input(ListCustomersInputSchema) - .output(ListCustomersOutputSchema); - -export const getCustomerContract = oc - .input(GetCustomerInputSchema) - .output(CustomerSchema); - -export const createCustomerContract = oc - .input(CreateCustomerInputSchema) - .output(CustomerSchema); - -export const updateCustomerContract = oc - .input(UpdateCustomerInputSchema) - .output(CustomerSchema); - -export const deleteCustomerContract = oc - .input(DeleteCustomerInputSchema) - .output(z.object({ ok: z.literal(true) })); - -export const customers = { - list: listCustomersContract, - get: getCustomerContract, - create: createCustomerContract, - update: updateCustomerContract, - delete: deleteCustomerContract, -}; diff --git a/src/contracts/mcp/index.ts b/src/contracts/mcp/index.ts deleted file mode 100644 index 2346cb5..0000000 --- a/src/contracts/mcp/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * MCP Contract namespace - separate from SDK contract. - * - * This contract is used by MCP tools for organization management. - * It uses OAuth authentication (not API key auth) and provides - * CRUD operations for customers, products, orders, and checkouts. - * - * NOTE: This is deliberately NOT exported to the SDK. It's only - * consumed by the MCP server via the dedicated /rpc/mcp endpoint. - */ - -export { customers } from "./customers"; -export { products } from "./products"; -export { orders } from "./orders"; -export { checkouts } from "./checkouts"; diff --git a/src/contracts/mcp/orders.ts b/src/contracts/mcp/orders.ts deleted file mode 100644 index a514ab6..0000000 --- a/src/contracts/mcp/orders.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { oc } from "@orpc/contract"; -import { z } from "zod"; -import { CustomerSchema } from "../../schemas/customer"; -import { OrderItemSchema, OrderSchema } from "../../schemas/order"; -import { - PaginationInputSchema, - PaginationOutputSchema, -} from "../../schemas/pagination"; - -// Order with related data for list view -const OrderWithRelationsSchema = OrderSchema.extend({ - customer: CustomerSchema.nullable(), - orderItems: z.array(OrderItemSchema), -}); - -// Order with full details for get view -const OrderDetailSchema = OrderWithRelationsSchema; - -const ListOrdersInputSchema = PaginationInputSchema.extend({ - customerId: z.string().optional(), - status: z.string().optional(), // Prisma uses String type for status -}); - -const ListOrdersOutputSchema = PaginationOutputSchema.extend({ - orders: z.array(OrderWithRelationsSchema), -}); - -export const listOrdersContract = oc - .input(ListOrdersInputSchema) - .output(ListOrdersOutputSchema); - -export const getOrderContract = oc - .input(z.object({ id: z.string() })) - .output(OrderDetailSchema); - -export const orders = { - list: listOrdersContract, - get: getOrderContract, -}; diff --git a/src/contracts/mcp/products.ts b/src/contracts/mcp/products.ts deleted file mode 100644 index 648ec72..0000000 --- a/src/contracts/mcp/products.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { oc } from "@orpc/contract"; -import { z } from "zod"; -import { - PaginationInputSchema, - PaginationOutputSchema, -} from "../../schemas/pagination"; -import { ProductPriceInputSchema } from "../../schemas/product-price-input"; -import { ProductPriceSchema, ProductSchema } from "../products"; - -// Output schema - product with its active price -// Note: Uses modifiedAt to match Prisma schema naming -const ProductWithPriceSchema = ProductSchema.omit({ prices: true }).extend({ - price: ProductPriceSchema.nullable(), - userMetadata: z.record(z.string(), z.any()).nullable(), - createdAt: z.date(), - modifiedAt: z.date().nullable(), -}); - -const ListProductsOutputSchema = PaginationOutputSchema.extend({ - products: z.array(ProductWithPriceSchema), -}); - -// Create input - NO benefitIds (not exposed on dashboard MCP) -const CreateProductInputSchema = z.object({ - name: z.string().min(1), - description: z.string().optional(), - price: ProductPriceInputSchema, - userMetadata: z.record(z.string(), z.string()).optional(), -}); - -// Update input - includes price (matches dashboard updateProduct), NO benefitIds -const UpdateProductInputSchema = z.object({ - id: z.string(), - name: z.string().min(1).optional(), - description: z.string().optional(), - price: ProductPriceInputSchema.optional(), // Can update pricing (immutable pattern applies) - userMetadata: z.record(z.string(), z.string()).optional(), -}); - -export const listProductsContract = oc - .input(PaginationInputSchema) - .output(ListProductsOutputSchema); - -export const getProductContract = oc - .input(z.object({ id: z.string() })) - .output(ProductWithPriceSchema); - -export const createProductContract = oc - .input(CreateProductInputSchema) - .output(ProductWithPriceSchema); - -export const updateProductContract = oc - .input(UpdateProductInputSchema) - .output(ProductWithPriceSchema); - -export const deleteProductContract = oc - .input(z.object({ id: z.string() })) - .output(z.object({ ok: z.literal(true) })); - -export const products = { - list: listProductsContract, - get: getProductContract, - create: createProductContract, - update: updateProductContract, - delete: deleteProductContract, -}; diff --git a/src/index.ts b/src/index.ts index 18f6d8d..e0821ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,5 @@ import { checkout } from "./contracts/checkout"; import { customer } from "./contracts/customer"; -import { - checkouts as mcpCheckouts, - customers as mcpCustomers, - orders as mcpOrders, - products as mcpProducts, -} from "./contracts/mcp"; import { onboarding } from "./contracts/onboarding"; import { order } from "./contracts/order"; import { products } from "./contracts/products"; @@ -59,21 +53,9 @@ export { RecurringIntervalInputSchema, } from "./schemas/product-price-input"; -// SDK contract - consumed by SDK clients +// Unified contract - consumed by both SDK and MCP export const contract = { checkout, customer, onboarding, order, products }; -/** - * MCP contract - separate namespace for MCP tools. - * NOT consumed by SDK, only by MCP server via /rpc/mcp endpoint. - * Uses OAuth authentication (not API key auth). - */ -export const mcpContract = { - customers: mcpCustomers, - products: mcpProducts, - orders: mcpOrders, - checkouts: mcpCheckouts, -}; - export type { MetadataValidationError } from "./validation/metadata-validation"; export { MAX_KEY_COUNT, From 1d8ac06d4c39472be868743466404eb247b4dcc9 Mon Sep 17 00:00:00 2001 From: npslaney Date: Mon, 26 Jan 2026 17:02:45 -0500 Subject: [PATCH 12/14] feat(api-contract): add SDK and MCP-specific contract exports Add sdkContract and mcpContract exports that contain only the methods each router implements. This allows proper TypeScript validation while keeping the unified contract for type sharing. - sdkContract: checkout.get/create/confirm/etc, onboarding.*, products.list - mcpContract: customer.*, order.*, checkout.list/get (summary), products.* - Add CheckoutListItemSchema and CheckoutDetailSchema for MCP endpoints - These are simpler than the full CheckoutSchema used by SDK Co-Authored-By: Claude Opus 4.5 --- src/contracts/checkout.ts | 54 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 28 +++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/contracts/checkout.ts b/src/contracts/checkout.ts index 7fbeff0..20fc02a 100644 --- a/src/contracts/checkout.ts +++ b/src/contracts/checkout.ts @@ -167,6 +167,58 @@ export const listCheckoutsContract = oc .input(ListCheckoutsInputSchema) .output(ListCheckoutsOutputSchema); +// MCP-specific embedded customer schema +const CheckoutCustomerSchema = z + .object({ + id: z.string(), + name: z.string().nullable(), + email: z.string().nullable(), + emailVerified: z.boolean(), + externalId: z.string().nullable(), + userMetadata: z.record(z.unknown()).nullable(), + organizationId: z.string(), + createdAt: z.date(), + modifiedAt: z.date().nullable(), + }) + .nullable(); + +// MCP-specific summary schema for list (simpler than full CheckoutSchema) +const CheckoutListItemSchema = z.object({ + id: z.string(), + status: CheckoutStatusSchema, + type: z.enum(["PRODUCTS", "AMOUNT", "TOP_UP"]), + 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 listCheckoutsSummaryContract = oc + .input(ListCheckoutsInputSchema) + .output(ListCheckoutsSummaryOutputSchema); + +export const getCheckoutSummaryContract = oc + .input(GetCheckoutInputSchema) + .output(CheckoutDetailSchema); + export const checkout = { get: getCheckoutContract, create: createCheckoutContract, @@ -174,4 +226,6 @@ export const checkout = { registerInvoice: registerInvoiceContract, paymentReceived: paymentReceivedContract, list: listCheckoutsContract, + listSummary: listCheckoutsSummaryContract, + getSummary: getCheckoutSummaryContract, }; diff --git a/src/index.ts b/src/index.ts index e0821ff..5c63438 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,9 +53,35 @@ export { RecurringIntervalInputSchema, } from "./schemas/product-price-input"; -// Unified contract - consumed by both SDK and MCP +// Unified contract - contains all methods from both SDK and MCP export const contract = { checkout, customer, onboarding, order, products }; +// SDK contract - only the methods the SDK router implements +export const sdkContract = { + checkout: { + get: checkout.get, + create: checkout.create, + confirm: checkout.confirm, + registerInvoice: checkout.registerInvoice, + paymentReceived: checkout.paymentReceived, + }, + onboarding, + products: { + list: products.list, + }, +}; + +// MCP contract - only the methods the MCP router implements +export const mcpContract = { + customer, + order, + checkout: { + list: checkout.listSummary, + get: checkout.getSummary, + }, + products, +}; + export type { MetadataValidationError } from "./validation/metadata-validation"; export { MAX_KEY_COUNT, From 7d4ed27b832d65a9cbe1fb5db8b213ba5747d495 Mon Sep 17 00:00:00 2001 From: npslaney Date: Tue, 27 Jan 2026 11:34:52 -0500 Subject: [PATCH 13/14] refactor: deduplicate schemas per PR review - Replace inline CheckoutCustomerSchema with CustomerSchema.nullable() - Remove unnecessary OrderDetailSchema alias Co-Authored-By: Claude Opus 4.5 --- src/contracts/checkout.ts | 15 ++------------- src/contracts/order.ts | 7 ++----- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/contracts/checkout.ts b/src/contracts/checkout.ts index 20fc02a..805c25d 100644 --- a/src/contracts/checkout.ts +++ b/src/contracts/checkout.ts @@ -1,6 +1,7 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; import { CheckoutSchema } from "../schemas/checkout"; +import { CustomerSchema } from "../schemas/customer"; import { CurrencySchema } from "../schemas/currency"; import { PaginationInputSchema, @@ -168,19 +169,7 @@ export const listCheckoutsContract = oc .output(ListCheckoutsOutputSchema); // MCP-specific embedded customer schema -const CheckoutCustomerSchema = z - .object({ - id: z.string(), - name: z.string().nullable(), - email: z.string().nullable(), - emailVerified: z.boolean(), - externalId: z.string().nullable(), - userMetadata: z.record(z.unknown()).nullable(), - organizationId: z.string(), - createdAt: z.date(), - modifiedAt: z.date().nullable(), - }) - .nullable(); +const CheckoutCustomerSchema = CustomerSchema.nullable(); // MCP-specific summary schema for list (simpler than full CheckoutSchema) const CheckoutListItemSchema = z.object({ diff --git a/src/contracts/order.ts b/src/contracts/order.ts index 065ce00..6ccdf8d 100644 --- a/src/contracts/order.ts +++ b/src/contracts/order.ts @@ -7,15 +7,12 @@ import { PaginationOutputSchema, } from "../schemas/pagination"; -// Order with related data for list view +// Order with related data for list and get views const OrderWithRelationsSchema = OrderSchema.extend({ customer: CustomerSchema.nullable(), orderItems: z.array(OrderItemSchema), }); -// Order with full details for get view -const OrderDetailSchema = OrderWithRelationsSchema; - const ListOrdersInputSchema = PaginationInputSchema.extend({ customerId: z.string().optional(), status: z.string().optional(), // Prisma uses String type for status @@ -31,7 +28,7 @@ export const listOrdersContract = oc export const getOrderContract = oc .input(z.object({ id: z.string() })) - .output(OrderDetailSchema); + .output(OrderWithRelationsSchema); export const order = { list: listOrdersContract, From f47999362b19d68f018bcc658972f12aa9d21965 Mon Sep 17 00:00:00 2001 From: npslaney Date: Tue, 27 Jan 2026 11:36:49 -0500 Subject: [PATCH 14/14] fix: correct import order in checkout.ts Co-Authored-By: Claude Opus 4.5 --- src/contracts/checkout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/checkout.ts b/src/contracts/checkout.ts index 805c25d..1cc9026 100644 --- a/src/contracts/checkout.ts +++ b/src/contracts/checkout.ts @@ -1,8 +1,8 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; import { CheckoutSchema } from "../schemas/checkout"; -import { CustomerSchema } from "../schemas/customer"; import { CurrencySchema } from "../schemas/currency"; +import { CustomerSchema } from "../schemas/customer"; import { PaginationInputSchema, PaginationOutputSchema,