Skip to content
10 changes: 10 additions & 0 deletions src/contracts/customer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { oc } from "@orpc/contract";
import { CustomerSchema, GetCustomerInputSchema } from "../schemas/customer";

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

export const customer = {
get: getCustomerContract,
};
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(),
});
Comment thread
NatElkins marked this conversation as resolved.

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,
};
39 changes: 38 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { checkout } from "./contracts/checkout";
import { customer } from "./contracts/customer";
import { onboarding } from "./contracts/onboarding";
import { products } from "./contracts/products";
import { subscription } from "./contracts/subscription";

export type {
ConfirmCheckout,
Expand All @@ -17,6 +19,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 @@ -27,8 +35,37 @@ 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,
} from "./schemas/customer";
export {
CustomerSchema,
CustomerSubscriptionSchema,
GetCustomerInputSchema,
} from "./schemas/customer";

export const contract = { checkout, onboarding, products };
export const contract = {
checkout,
customer,
onboarding,
products,
subscription,
};

export type { MetadataValidationError } from "./validation/metadata-validation";
export {
Expand Down
62 changes: 62 additions & 0 deletions src/schemas/customer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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(), // ISO date
Comment thread
NatElkins marked this conversation as resolved.
Outdated
currentPeriodEnd: z.string(), // ISO date
cancelAtPeriodEnd: z.boolean().optional(),
amount: z.number(),
currency: CurrencySchema,
recurringInterval: RecurringIntervalSchema,
});

/**
* Customer data with their subscriptions.
* Returned by the 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),
hasActiveSubscription: z.boolean(),
Comment thread
NatElkins marked this conversation as resolved.
Outdated
});

/**
* Input for getting a customer.
* 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",
},
);

export type CustomerSubscription = z.infer<typeof CustomerSubscriptionSchema>;
export type Customer = z.infer<typeof CustomerSchema>;
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(),
Comment thread
NatElkins marked this conversation as resolved.
Outdated
productId: z.string(),
amount: z.number(),
currency: CurrencySchema,
recurringInterval: RecurringIntervalSchema,
status: SubscriptionStatusSchema,
currentPeriodStart: z.string(), // ISO date
currentPeriodEnd: z.string(), // ISO date
cancelAtPeriodEnd: z.boolean().optional(),
endsAt: z.string().optional(), // ISO date (if scheduled to end)
endedAt: z.string().optional(), // ISO date (if ended)
canceledAt: z.string().optional(), // ISO date (if canceled)
startedAt: z.string(), // ISO date
});

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
>;