Skip to content

Commit 3616078

Browse files
authored
feat: add subscription and customer schemas and contracts (MDK-401)
* feat: add subscription schemas and contracts Add subscription support for recurring payments: - SubscriptionSchema with status, period dates, and cancellation fields - SubscriptionWebhookPayloadSchema for subscription.* events - Subscription ORPC contracts (createRenewalCheckout, cancel, get) * feat: add customer schemas and contracts Add customer endpoint support for subscription management: - CustomerSchema with subscriptions and hasActiveSubscription - CustomerSubscriptionSchema for subscription summaries - GetCustomerInputSchema with externalId/email/customerId lookup - customer.get ORPC contract * fix: require email in CustomerSchema for subscription communication * feat: add prepare script for git installs * fix: allow nullable email in Customer schema * fix: add datetime validation to ISO date fields, remove hasActiveSubscription - Add z.string().datetime() validation to currentPeriodStart, currentPeriodEnd in SubscriptionSchema and CustomerSubscriptionSchema - Remove hasActiveSubscription from CustomerSchema as it can be derived from subscriptions array client-side * fix: make customerEmail nullable in SubscriptionSchema Allows the schema to handle edge cases while business logic enforces email requirement at subscription creation time. * fix: format code with biome * fix: add customer to sdkContract
1 parent b2ff3f5 commit 3616078

6 files changed

Lines changed: 225 additions & 12 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
},
2222
"scripts": {
2323
"build": "tsup",
24+
"prepare": "pnpm run build",
2425
"test": "vitest",
2526
"check": "biome check ./src --fix"
2627
},

src/contracts/customer.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import { oc } from "@orpc/contract";
22
import { z } from "zod";
3-
import { CustomerSchema } from "../schemas/customer";
3+
import {
4+
CustomerSchema,
5+
McpCustomerSchema,
6+
GetCustomerInputSchema as SdkGetCustomerInputSchema,
7+
} from "../schemas/customer";
48
import {
59
PaginationInputSchema,
610
PaginationOutputSchema,
711
} from "../schemas/pagination";
812

13+
// MCP-specific schemas
914
const ListCustomersInputSchema = PaginationInputSchema;
1015
const ListCustomersOutputSchema = PaginationOutputSchema.extend({
11-
customers: z.array(CustomerSchema),
16+
customers: z.array(McpCustomerSchema),
1217
});
1318

14-
const GetCustomerInputSchema = z.object({ id: z.string() });
19+
const McpGetCustomerInputSchema = z.object({ id: z.string() });
1520

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

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

35+
// SDK contract - uses flexible lookup (externalId/email/customerId)
36+
export const getSdkCustomerContract = oc
37+
.input(SdkGetCustomerInputSchema)
38+
.output(CustomerSchema);
39+
40+
// MCP contracts
3041
export const listCustomersContract = oc
3142
.input(ListCustomersInputSchema)
3243
.output(ListCustomersOutputSchema);
3344

3445
export const getCustomerContract = oc
35-
.input(GetCustomerInputSchema)
36-
.output(CustomerSchema);
46+
.input(McpGetCustomerInputSchema)
47+
.output(McpCustomerSchema);
3748

3849
export const createCustomerContract = oc
3950
.input(CreateCustomerInputSchema)
40-
.output(CustomerSchema);
51+
.output(McpCustomerSchema);
4152

4253
export const updateCustomerContract = oc
4354
.input(UpdateCustomerInputSchema)
44-
.output(CustomerSchema);
55+
.output(McpCustomerSchema);
4556

4657
export const deleteCustomerContract = oc
4758
.input(DeleteCustomerInputSchema)
@@ -50,6 +61,7 @@ export const deleteCustomerContract = oc
5061
export const customer = {
5162
list: listCustomersContract,
5263
get: getCustomerContract,
64+
getSdk: getSdkCustomerContract,
5365
create: createCustomerContract,
5466
update: updateCustomerContract,
5567
delete: deleteCustomerContract,

src/contracts/subscription.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { oc } from "@orpc/contract";
2+
import { z } from "zod";
3+
import { SubscriptionSchema } from "../schemas/subscription";
4+
5+
export const CreateRenewalCheckoutInputSchema = z.object({
6+
subscriptionId: z.string(),
7+
});
8+
9+
export const CreateRenewalCheckoutOutputSchema = z.object({
10+
checkoutId: z.string(),
11+
});
12+
13+
export const CancelSubscriptionInputSchema = z.object({
14+
subscriptionId: z.string(),
15+
});
16+
17+
export const CancelSubscriptionOutputSchema = z.object({
18+
ok: z.boolean(),
19+
});
20+
21+
export const GetSubscriptionInputSchema = z.object({
22+
subscriptionId: z.string(),
23+
});
24+
25+
export type CreateRenewalCheckout = z.infer<
26+
typeof CreateRenewalCheckoutInputSchema
27+
>;
28+
export type CancelSubscriptionInput = z.infer<
29+
typeof CancelSubscriptionInputSchema
30+
>;
31+
export type GetSubscriptionInput = z.infer<typeof GetSubscriptionInputSchema>;
32+
33+
export const createRenewalCheckoutContract = oc
34+
.input(CreateRenewalCheckoutInputSchema)
35+
.output(CreateRenewalCheckoutOutputSchema);
36+
37+
export const cancelSubscriptionContract = oc
38+
.input(CancelSubscriptionInputSchema)
39+
.output(CancelSubscriptionOutputSchema);
40+
41+
export const getSubscriptionContract = oc
42+
.input(GetSubscriptionInputSchema)
43+
.output(SubscriptionSchema);
44+
45+
export const subscription = {
46+
createRenewalCheckout: createRenewalCheckoutContract,
47+
cancel: cancelSubscriptionContract,
48+
get: getSubscriptionContract,
49+
};

src/index.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { customer } from "./contracts/customer";
33
import { onboarding } from "./contracts/onboarding";
44
import { order } from "./contracts/order";
55
import { products } from "./contracts/products";
6+
import { subscription } from "./contracts/subscription";
67

78
export type {
89
ConfirmCheckout,
@@ -19,6 +20,12 @@ export type {
1920
StartDeviceAuth as StartDeviceAuthInput,
2021
StartDeviceAuthResponse,
2122
} from "./contracts/onboarding";
23+
export type {
24+
CancelSubscriptionInput,
25+
CreateRenewalCheckout,
26+
GetSubscriptionInput,
27+
} from "./contracts/subscription";
28+
export type { GetCustomerInput } from "./schemas/customer";
2229
export type { Checkout } from "./schemas/checkout";
2330
export { CheckoutSchema } from "./schemas/checkout";
2431
export type { Currency } from "./schemas/currency";
@@ -29,10 +36,33 @@ export {
2936
ProductPriceSchema,
3037
ListProductsOutputSchema,
3138
} from "./contracts/products";
39+
export type {
40+
RecurringInterval,
41+
Subscription,
42+
SubscriptionStatus,
43+
SubscriptionWebhookEvent,
44+
SubscriptionWebhookPayload,
45+
} from "./schemas/subscription";
46+
export {
47+
RecurringIntervalSchema,
48+
SubscriptionSchema,
49+
SubscriptionStatusSchema,
50+
SubscriptionWebhookEventSchema,
51+
SubscriptionWebhookPayloadSchema,
52+
} from "./schemas/subscription";
53+
export type {
54+
Customer,
55+
CustomerSubscription,
56+
McpCustomer,
57+
} from "./schemas/customer";
58+
export {
59+
CustomerSchema,
60+
CustomerSubscriptionSchema,
61+
GetCustomerInputSchema,
62+
McpCustomerSchema,
63+
} from "./schemas/customer";
3264

3365
// New MCP schemas
34-
export type { Customer } from "./schemas/customer";
35-
export { CustomerSchema } from "./schemas/customer";
3666
export type { Order, OrderItem, OrderStatus } from "./schemas/order";
3767
export {
3868
OrderSchema,
@@ -54,7 +84,14 @@ export {
5484
} from "./schemas/product-price-input";
5585

5686
// Unified contract - contains all methods from both SDK and MCP
57-
export const contract = { checkout, customer, onboarding, order, products };
87+
export const contract = {
88+
checkout,
89+
customer,
90+
onboarding,
91+
order,
92+
products,
93+
subscription,
94+
};
5895

5996
// SDK contract - only the methods the SDK router implements
6097
export const sdkContract = {
@@ -65,10 +102,14 @@ export const sdkContract = {
65102
registerInvoice: checkout.registerInvoice,
66103
paymentReceived: checkout.paymentReceived,
67104
},
105+
customer: {
106+
get: customer.getSdk,
107+
},
68108
onboarding,
69109
products: {
70110
list: products.list,
71111
},
112+
subscription,
72113
};
73114

74115
// MCP contract - only the methods the MCP router implements

src/schemas/customer.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,67 @@
11
import { z } from "zod";
2+
import { CurrencySchema } from "./currency";
3+
import {
4+
RecurringIntervalSchema,
5+
SubscriptionStatusSchema,
6+
} from "./subscription";
7+
8+
/**
9+
* Summary of a subscription for the customer response.
10+
* Contains the essential fields needed for displaying subscription status.
11+
*/
12+
export const CustomerSubscriptionSchema = z.object({
13+
id: z.string(),
14+
productId: z.string(),
15+
status: SubscriptionStatusSchema,
16+
currentPeriodStart: z.string().datetime(),
17+
currentPeriodEnd: z.string().datetime(),
18+
cancelAtPeriodEnd: z.boolean().optional(),
19+
amount: z.number(),
20+
currency: CurrencySchema,
21+
recurringInterval: RecurringIntervalSchema,
22+
});
23+
24+
/**
25+
* Customer data with their subscriptions.
26+
* Returned by the SDK customer.get endpoint.
27+
*/
28+
export const CustomerSchema = z.object({
29+
id: z.string(),
30+
email: z.string().nullable().optional(),
31+
name: z.string().nullable().optional(),
32+
externalId: z.string().nullable().optional(),
33+
subscriptions: z.array(CustomerSubscriptionSchema),
34+
});
35+
36+
/**
37+
* Input for getting a customer via SDK.
38+
* Requires exactly one of: externalId, email, or customerId.
39+
*/
40+
export const GetCustomerInputSchema = z
41+
.object({
42+
externalId: z.string().optional(),
43+
email: z.string().optional(),
44+
customerId: z.string().optional(),
45+
})
46+
.refine(
47+
(data) => {
48+
const fields = [data.externalId, data.email, data.customerId].filter(
49+
Boolean,
50+
);
51+
return fields.length === 1;
52+
},
53+
{
54+
message:
55+
"Exactly one of externalId, email, or customerId must be provided",
56+
},
57+
);
258

359
/**
460
* Customer schema for MCP API responses.
5-
* Represents a customer in the organization.
61+
* Represents a customer in the organization (admin view).
662
* Note: Uses modifiedAt to match Prisma schema naming.
763
*/
8-
export const CustomerSchema = z.object({
64+
export const McpCustomerSchema = z.object({
965
id: z.string(),
1066
name: z.string().nullable(),
1167
email: z.string().nullable(),
@@ -17,4 +73,7 @@ export const CustomerSchema = z.object({
1773
modifiedAt: z.date().nullable(),
1874
});
1975

76+
export type CustomerSubscription = z.infer<typeof CustomerSubscriptionSchema>;
2077
export type Customer = z.infer<typeof CustomerSchema>;
78+
export type McpCustomer = z.infer<typeof McpCustomerSchema>;
79+
export type GetCustomerInput = z.infer<typeof GetCustomerInputSchema>;

src/schemas/subscription.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { z } from "zod";
2+
import { CurrencySchema } from "./currency";
3+
4+
export const SubscriptionStatusSchema = z.enum([
5+
"active",
6+
"past_due",
7+
"canceled",
8+
]);
9+
10+
export const RecurringIntervalSchema = z.enum(["MONTH", "QUARTER", "YEAR"]);
11+
12+
export const SubscriptionSchema = z.object({
13+
id: z.string(),
14+
customerId: z.string(),
15+
customerEmail: z.string().nullable(),
16+
productId: z.string(),
17+
amount: z.number(),
18+
currency: CurrencySchema,
19+
recurringInterval: RecurringIntervalSchema,
20+
status: SubscriptionStatusSchema,
21+
currentPeriodStart: z.string().datetime(),
22+
currentPeriodEnd: z.string().datetime(),
23+
cancelAtPeriodEnd: z.boolean().optional(),
24+
endsAt: z.string().datetime().optional(),
25+
endedAt: z.string().datetime().optional(),
26+
canceledAt: z.string().datetime().optional(),
27+
startedAt: z.string().datetime(),
28+
});
29+
30+
export const SubscriptionWebhookEventSchema = z.enum([
31+
"subscription.created",
32+
"subscription.renewed",
33+
"subscription.canceled",
34+
"subscription.payment_failed",
35+
]);
36+
37+
export const SubscriptionWebhookPayloadSchema = z.object({
38+
handler: z.literal("webhooks"),
39+
event: SubscriptionWebhookEventSchema,
40+
subscription: SubscriptionSchema,
41+
});
42+
43+
export type Subscription = z.infer<typeof SubscriptionSchema>;
44+
export type SubscriptionStatus = z.infer<typeof SubscriptionStatusSchema>;
45+
export type RecurringInterval = z.infer<typeof RecurringIntervalSchema>;
46+
export type SubscriptionWebhookEvent = z.infer<
47+
typeof SubscriptionWebhookEventSchema
48+
>;
49+
export type SubscriptionWebhookPayload = z.infer<
50+
typeof SubscriptionWebhookPayloadSchema
51+
>;

0 commit comments

Comments
 (0)