diff --git a/package.json b/package.json index e62299d..f538327 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "scripts": { "build": "tsup", + "prepare": "pnpm run build", "test": "vitest", "check": "biome check ./src --fix" }, diff --git a/src/contracts/customer.ts b/src/contracts/customer.ts index 699edfc..467ce89 100644 --- a/src/contracts/customer.ts +++ b/src/contracts/customer.ts @@ -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), @@ -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) @@ -50,6 +61,7 @@ export const deleteCustomerContract = oc export const customer = { list: listCustomersContract, get: getCustomerContract, + getSdk: getSdkCustomerContract, create: createCustomerContract, update: updateCustomerContract, delete: deleteCustomerContract, diff --git a/src/contracts/subscription.ts b/src/contracts/subscription.ts new file mode 100644 index 0000000..a0af9e2 --- /dev/null +++ b/src/contracts/subscription.ts @@ -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; + +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, +}; diff --git a/src/index.ts b/src/index.ts index 5c63438..dda2775 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, @@ -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"; @@ -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, @@ -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 = { @@ -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 diff --git a/src/schemas/customer.ts b/src/schemas/customer.ts index 5eb4dbc..125309c 100644 --- a/src/schemas/customer.ts +++ b/src/schemas/customer.ts @@ -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(), @@ -17,4 +73,7 @@ export const CustomerSchema = z.object({ modifiedAt: z.date().nullable(), }); +export type CustomerSubscription = z.infer; export type Customer = z.infer; +export type McpCustomer = z.infer; +export type GetCustomerInput = z.infer; diff --git a/src/schemas/subscription.ts b/src/schemas/subscription.ts new file mode 100644 index 0000000..c97d7c1 --- /dev/null +++ b/src/schemas/subscription.ts @@ -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; +export type SubscriptionStatus = z.infer; +export type RecurringInterval = z.infer; +export type SubscriptionWebhookEvent = z.infer< + typeof SubscriptionWebhookEventSchema +>; +export type SubscriptionWebhookPayload = z.infer< + typeof SubscriptionWebhookPayloadSchema +>;