Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
68 changes: 68 additions & 0 deletions src/contracts/mcp/checkouts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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,
};
56 changes: 56 additions & 0 deletions src/contracts/mcp/customers.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(),
});

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,
};
15 changes: 15 additions & 0 deletions src/contracts/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -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";
39 changes: 39 additions & 0 deletions src/contracts/mcp/orders.ts
Original file line number Diff line number Diff line change
@@ -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 orders = {
list: listOrdersContract,
get: getOrderContract,
};
66 changes: 66 additions & 0 deletions src/contracts/mcp/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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,
};
32 changes: 32 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { checkout } from "./contracts/checkout";
import {
checkouts as mcpCheckouts,
customers as mcpCustomers,
orders as mcpOrders,
products as mcpProducts,
} from "./contracts/mcp";
import { onboarding } from "./contracts/onboarding";
import { products } from "./contracts/products";

Expand Down Expand Up @@ -28,8 +34,34 @@ 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,
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>;
50 changes: 50 additions & 0 deletions src/schemas/order.ts
Original file line number Diff line number Diff line change
@@ -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<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: z.string(), // Prisma uses String, not enum
Comment thread
npslaney marked this conversation as resolved.
Outdated
currency: z.string(),
Comment thread
npslaney marked this conversation as resolved.
Outdated
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