Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/contracts/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { oc } from "@orpc/contract";
import { z } from "zod";
import { CheckoutSchema } from "../schemas/checkout";
import { CurrencySchema } from "../schemas/currency";
import { CustomerSchema } from "../schemas/customer";
import {
PaginationInputSchema,
PaginationOutputSchema,
} from "../schemas/pagination";

/**
* Helper to treat empty strings as undefined (not provided).
Expand Down Expand Up @@ -142,10 +147,74 @@ 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);

// MCP-specific embedded customer schema
const CheckoutCustomerSchema = CustomerSchema.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,
confirm: confirmCheckoutContract,
registerInvoice: registerInvoiceContract,
paymentReceived: paymentReceivedContract,
list: listCheckoutsContract,
listSummary: listCheckoutsSummaryContract,
getSummary: getCheckoutSummaryContract,
};
56 changes: 56 additions & 0 deletions src/contracts/customer.ts
Original file line number Diff line number Diff line change
@@ -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(),
Copy link
Contributor

@amackillop amackillop Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have metadata defined on many schemas but in many different ways. I think we should have a single Metadata schema that gets re-used. Can be a future ticket as it is probably an existing problem

});

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,
};
36 changes: 36 additions & 0 deletions src/contracts/order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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 and get views
const OrderWithRelationsSchema = OrderSchema.extend({
customer: CustomerSchema.nullable(),
orderItems: z.array(OrderItemSchema),
});

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(OrderWithRelationsSchema);

export const order = {
list: listOrdersContract,
get: getOrderContract,
};
37 changes: 37 additions & 0 deletions src/contracts/products.ts
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down Expand Up @@ -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,
};
54 changes: 53 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { checkout } from "./contracts/checkout";
import { customer } from "./contracts/customer";
import { onboarding } from "./contracts/onboarding";
import { order } from "./contracts/order";
import { products } from "./contracts/products";

export type {
Expand Down Expand Up @@ -28,7 +30,57 @@ export {
ListProductsOutputSchema,
} from "./contracts/products";

export const contract = { checkout, onboarding, products };
// New MCP schemas
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 type { PaginationInput, PaginationOutput } from "./schemas/pagination";
export {
PaginationInputSchema,
PaginationOutputSchema,
} from "./schemas/pagination";
export type {
ProductPriceInput,
RecurringIntervalInput,
} from "./schemas/product-price-input";
export {
ProductPriceInputSchema,
RecurringIntervalInputSchema,
} from "./schemas/product-price-input";

// 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 {
Expand Down
20 changes: 20 additions & 0 deletions src/schemas/customer.ts
Original file line number Diff line number Diff line change
@@ -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<typeof CustomerSchema>;
51 changes: 51 additions & 0 deletions src/schemas/order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from "zod";
import { CurrencySchema } from "./currency";

/**
* 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<typeof OrderStatusSchema>;

/**
* 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<typeof OrderItemSchema>;

/**
* 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: OrderStatusSchema,
currency: CurrencySchema,
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<typeof OrderSchema>;
22 changes: 22 additions & 0 deletions src/schemas/pagination.ts
Original file line number Diff line number Diff line change
@@ -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<typeof PaginationInputSchema>;

/**
* 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<typeof PaginationOutputSchema>;
Loading