Skip to content
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"scripts": {
"build": "tsup",
"prepare": "pnpm run build",
"test": "vitest",
"check": "biome check ./src --fix"
},
Expand Down
26 changes: 19 additions & 7 deletions src/contracts/customer.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { oc } from "@orpc/contract";
import { z } from "zod";
import { CustomerSchema } from "../schemas/customer";
import {
CustomerSchema,
McpCustomerSchema,
GetCustomerInputSchema as SdkGetCustomerInputSchema,
} from "../schemas/customer";
import {
PaginationInputSchema,
PaginationOutputSchema,
} from "../schemas/pagination";

// MCP-specific schemas
const ListCustomersInputSchema = PaginationInputSchema;
const ListCustomersOutputSchema = PaginationOutputSchema.extend({
customers: z.array(CustomerSchema),
customers: z.array(McpCustomerSchema),
});

const GetCustomerInputSchema = z.object({ id: z.string() });
const McpGetCustomerInputSchema = z.object({ id: z.string() });

const CreateCustomerInputSchema = z.object({
name: z.string().min(1),
Expand All @@ -27,21 +32,27 @@ const UpdateCustomerInputSchema = z.object({

const DeleteCustomerInputSchema = z.object({ id: z.string() });

// SDK contract - uses flexible lookup (externalId/email/customerId)
export const getSdkCustomerContract = oc
.input(SdkGetCustomerInputSchema)
.output(CustomerSchema);

// MCP contracts
export const listCustomersContract = oc
.input(ListCustomersInputSchema)
.output(ListCustomersOutputSchema);

export const getCustomerContract = oc
.input(GetCustomerInputSchema)
.output(CustomerSchema);
.input(McpGetCustomerInputSchema)
.output(McpCustomerSchema);

export const createCustomerContract = oc
.input(CreateCustomerInputSchema)
.output(CustomerSchema);
.output(McpCustomerSchema);

export const updateCustomerContract = oc
.input(UpdateCustomerInputSchema)
.output(CustomerSchema);
.output(McpCustomerSchema);

export const deleteCustomerContract = oc
.input(DeleteCustomerInputSchema)
Expand All @@ -50,6 +61,7 @@ export const deleteCustomerContract = oc
export const customer = {
list: listCustomersContract,
get: getCustomerContract,
getSdk: getSdkCustomerContract,
create: createCustomerContract,
update: updateCustomerContract,
delete: deleteCustomerContract,
Expand Down
49 changes: 49 additions & 0 deletions src/contracts/subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { oc } from "@orpc/contract";
import { z } from "zod";
import { SubscriptionSchema } from "../schemas/subscription";

export const CreateRenewalCheckoutInputSchema = z.object({
subscriptionId: z.string(),
});

export const CreateRenewalCheckoutOutputSchema = z.object({
checkoutId: z.string(),
});

export const CancelSubscriptionInputSchema = z.object({
subscriptionId: z.string(),
});

export const CancelSubscriptionOutputSchema = z.object({
ok: z.boolean(),
});

export const GetSubscriptionInputSchema = z.object({
subscriptionId: z.string(),
});

export type CreateRenewalCheckout = z.infer<
typeof CreateRenewalCheckoutInputSchema
>;
export type CancelSubscriptionInput = z.infer<
typeof CancelSubscriptionInputSchema
>;
export type GetSubscriptionInput = z.infer<typeof GetSubscriptionInputSchema>;

export const createRenewalCheckoutContract = oc
.input(CreateRenewalCheckoutInputSchema)
.output(CreateRenewalCheckoutOutputSchema);

export const cancelSubscriptionContract = oc
.input(CancelSubscriptionInputSchema)
.output(CancelSubscriptionOutputSchema);

export const getSubscriptionContract = oc
.input(GetSubscriptionInputSchema)
.output(SubscriptionSchema);

export const subscription = {
createRenewalCheckout: createRenewalCheckoutContract,
cancel: cancelSubscriptionContract,
get: getSubscriptionContract,
};
47 changes: 44 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { customer } from "./contracts/customer";
import { onboarding } from "./contracts/onboarding";
import { order } from "./contracts/order";
import { products } from "./contracts/products";
import { subscription } from "./contracts/subscription";

export type {
ConfirmCheckout,
Expand All @@ -19,6 +20,12 @@ export type {
StartDeviceAuth as StartDeviceAuthInput,
StartDeviceAuthResponse,
} from "./contracts/onboarding";
export type {
CancelSubscriptionInput,
CreateRenewalCheckout,
GetSubscriptionInput,
} from "./contracts/subscription";
export type { GetCustomerInput } from "./schemas/customer";
export type { Checkout } from "./schemas/checkout";
export { CheckoutSchema } from "./schemas/checkout";
export type { Currency } from "./schemas/currency";
Expand All @@ -29,10 +36,33 @@ export {
ProductPriceSchema,
ListProductsOutputSchema,
} from "./contracts/products";
export type {
RecurringInterval,
Subscription,
SubscriptionStatus,
SubscriptionWebhookEvent,
SubscriptionWebhookPayload,
} from "./schemas/subscription";
export {
RecurringIntervalSchema,
SubscriptionSchema,
SubscriptionStatusSchema,
SubscriptionWebhookEventSchema,
SubscriptionWebhookPayloadSchema,
} from "./schemas/subscription";
export type {
Customer,
CustomerSubscription,
McpCustomer,
} from "./schemas/customer";
export {
CustomerSchema,
CustomerSubscriptionSchema,
GetCustomerInputSchema,
McpCustomerSchema,
} from "./schemas/customer";

// New MCP schemas
export type { Customer } from "./schemas/customer";
export { CustomerSchema } from "./schemas/customer";
export type { Order, OrderItem, OrderStatus } from "./schemas/order";
export {
OrderSchema,
Expand All @@ -54,7 +84,14 @@ export {
} from "./schemas/product-price-input";

// Unified contract - contains all methods from both SDK and MCP
export const contract = { checkout, customer, onboarding, order, products };
export const contract = {
checkout,
customer,
onboarding,
order,
products,
subscription,
};

// SDK contract - only the methods the SDK router implements
export const sdkContract = {
Expand All @@ -65,10 +102,14 @@ export const sdkContract = {
registerInvoice: checkout.registerInvoice,
paymentReceived: checkout.paymentReceived,
},
customer: {
get: customer.getSdk,
},
onboarding,
products: {
list: products.list,
},
subscription,
};

// MCP contract - only the methods the MCP router implements
Expand Down
63 changes: 61 additions & 2 deletions src/schemas/customer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,67 @@
import { z } from "zod";
import { CurrencySchema } from "./currency";
import {
RecurringIntervalSchema,
SubscriptionStatusSchema,
} from "./subscription";

/**
* Summary of a subscription for the customer response.
* Contains the essential fields needed for displaying subscription status.
*/
export const CustomerSubscriptionSchema = z.object({
id: z.string(),
productId: z.string(),
status: SubscriptionStatusSchema,
currentPeriodStart: z.string().datetime(),
currentPeriodEnd: z.string().datetime(),
cancelAtPeriodEnd: z.boolean().optional(),
amount: z.number(),
currency: CurrencySchema,
recurringInterval: RecurringIntervalSchema,
});

/**
* Customer data with their subscriptions.
* Returned by the SDK customer.get endpoint.
*/
export const CustomerSchema = z.object({
id: z.string(),
email: z.string().nullable().optional(),
name: z.string().nullable().optional(),
externalId: z.string().nullable().optional(),
subscriptions: z.array(CustomerSubscriptionSchema),
});

/**
* Input for getting a customer via SDK.
* Requires exactly one of: externalId, email, or customerId.
*/
export const GetCustomerInputSchema = z
.object({
externalId: z.string().optional(),
email: z.string().optional(),
customerId: z.string().optional(),
})
.refine(
(data) => {
const fields = [data.externalId, data.email, data.customerId].filter(
Boolean,
);
return fields.length === 1;
},
{
message:
"Exactly one of externalId, email, or customerId must be provided",
},
);

/**
* Customer schema for MCP API responses.
* Represents a customer in the organization.
* Represents a customer in the organization (admin view).
* Note: Uses modifiedAt to match Prisma schema naming.
*/
export const CustomerSchema = z.object({
export const McpCustomerSchema = z.object({
id: z.string(),
name: z.string().nullable(),
email: z.string().nullable(),
Expand All @@ -17,4 +73,7 @@ export const CustomerSchema = z.object({
modifiedAt: z.date().nullable(),
});

export type CustomerSubscription = z.infer<typeof CustomerSubscriptionSchema>;
export type Customer = z.infer<typeof CustomerSchema>;
export type McpCustomer = z.infer<typeof McpCustomerSchema>;
export type GetCustomerInput = z.infer<typeof GetCustomerInputSchema>;
51 changes: 51 additions & 0 deletions src/schemas/subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from "zod";
import { CurrencySchema } from "./currency";

export const SubscriptionStatusSchema = z.enum([
"active",
"past_due",
"canceled",
]);

export const RecurringIntervalSchema = z.enum(["MONTH", "QUARTER", "YEAR"]);

export const SubscriptionSchema = z.object({
id: z.string(),
customerId: z.string(),
customerEmail: z.string().nullable(),
productId: z.string(),
amount: z.number(),
currency: CurrencySchema,
recurringInterval: RecurringIntervalSchema,
status: SubscriptionStatusSchema,
currentPeriodStart: z.string().datetime(),
currentPeriodEnd: z.string().datetime(),
cancelAtPeriodEnd: z.boolean().optional(),
endsAt: z.string().datetime().optional(),
endedAt: z.string().datetime().optional(),
canceledAt: z.string().datetime().optional(),
startedAt: z.string().datetime(),
});

export const SubscriptionWebhookEventSchema = z.enum([
"subscription.created",
"subscription.renewed",
"subscription.canceled",
"subscription.payment_failed",
]);

export const SubscriptionWebhookPayloadSchema = z.object({
handler: z.literal("webhooks"),
event: SubscriptionWebhookEventSchema,
subscription: SubscriptionSchema,
});

export type Subscription = z.infer<typeof SubscriptionSchema>;
export type SubscriptionStatus = z.infer<typeof SubscriptionStatusSchema>;
export type RecurringInterval = z.infer<typeof RecurringIntervalSchema>;
export type SubscriptionWebhookEvent = z.infer<
typeof SubscriptionWebhookEventSchema
>;
export type SubscriptionWebhookPayload = z.infer<
typeof SubscriptionWebhookPayloadSchema
>;