← Public packages
@kentcdodds/stripe
Stripe helpers for customers, payments, invoices, subscriptions, products, payment links, refunds, and balance.
src/subscriptions.ts
157 lines · 4.9 KB · TypeScriptimport {
formatStripeAmount,
idOf,
requireConfirm,
stripeDate,
stripeList,
stripeRequest,
stripeSearch,
} from './stripe-core.ts'
export function summarizeSubscription(subscription: any) {
if (!subscription || typeof subscription !== 'object') return null
const items = subscription.items?.data ?? []
return {
id: subscription.id ?? null,
status: subscription.status ?? null,
customerId: idOf(subscription.customer),
created: stripeDate(subscription.created),
currentPeriodEnd: stripeDate(
subscription.current_period_end ?? items[0]?.current_period_end,
),
cancelAtPeriodEnd: Boolean(subscription.cancel_at_period_end),
canceledAt: stripeDate(subscription.canceled_at),
items: items.map((item: any) => ({
id: item.id,
priceId: idOf(item.price),
productId: idOf(item.price?.product),
quantity: item.quantity ?? null,
unitAmount: item.price?.unit_amount ?? null,
currency: item.price?.currency ?? null,
display: formatStripeAmount(item.price?.unit_amount, item.price?.currency),
interval: item.price?.recurring?.interval ?? null,
})),
metadata: subscription.metadata ?? {},
}
}
export type ListSubscriptionsInput = {
customerId?: string
/** Defaults to Stripe's default (excludes canceled); pass 'all' to include everything. */
status?:
| 'active'
| 'past_due'
| 'unpaid'
| 'canceled'
| 'incomplete'
| 'incomplete_expired'
| 'trialing'
| 'paused'
| 'all'
priceId?: string
maxItems?: number
}
/** List subscriptions, optionally filtered by customer, status, or price. */
export async function listSubscriptions(input: ListSubscriptionsInput = {}) {
const { items, hasMore } = await stripeList('subscriptions', {
maxItems: input.maxItems ?? 25,
query: { customer: input.customerId, status: input.status, price: input.priceId },
})
return { hasMore, subscriptions: items.map(summarizeSubscription) }
}
/** Get one subscription by id (full Stripe object). */
export async function getSubscription(input: { subscriptionId: string }) {
return await stripeRequest({ path: 'subscriptions/' + input.subscriptionId })
}
/**
* Search subscriptions with Stripe's search query language.
* @example searchSubscriptions({ searchQuery: "status:'active' AND metadata['plan']:'pro'" })
*/
export async function searchSubscriptions(input: { searchQuery: string; maxItems?: number }) {
const { items, hasMore } = await stripeSearch('subscriptions/search', {
searchQuery: input.searchQuery,
maxItems: input.maxItems ?? 25,
})
return { hasMore, subscriptions: items.map(summarizeSubscription) }
}
export type CreateSubscriptionInput = {
customerId: string
priceId: string
quantity?: number
trialDays?: number
metadata?: Record<string, string>
idempotencyKey?: string
/** Subscriptions bill real money; must be true. */
confirm?: boolean
}
/** Create a subscription for an existing customer + price. Requires confirm: true. */
export async function createSubscription(input: CreateSubscriptionInput) {
requireConfirm(input, 'create a subscription for ' + input.customerId)
const subscription = await stripeRequest({
path: 'subscriptions',
method: 'POST',
idempotencyKey: input.idempotencyKey,
body: {
customer: input.customerId,
items: [{ price: input.priceId, quantity: input.quantity ?? 1 }],
trial_period_days: input.trialDays,
metadata: input.metadata,
},
})
return summarizeSubscription(subscription)
}
export type CancelSubscriptionInput = {
subscriptionId: string
/** Default true: cancel at period end instead of immediately. */
atPeriodEnd?: boolean
confirm?: boolean
}
/** Cancel a subscription (at period end by default). Requires confirm: true. */
export async function cancelSubscription(input: CancelSubscriptionInput) {
requireConfirm(input, 'cancel subscription ' + input.subscriptionId)
if (input.atPeriodEnd === false) {
return summarizeSubscription(
await stripeRequest({
path: 'subscriptions/' + input.subscriptionId,
method: 'DELETE',
}),
)
}
return summarizeSubscription(
await stripeRequest({
path: 'subscriptions/' + input.subscriptionId,
method: 'POST',
body: { cancel_at_period_end: true },
}),
)
}
/**
* Subscriptions dispatcher. Defaults to list-subscriptions.
* Actions: list-subscriptions, get-subscription, search-subscriptions,
* create-subscription, cancel-subscription.
*/
export default async function subscriptions(input: Record<string, unknown> = {}) {
const action = String(input.action ?? 'list-subscriptions')
switch (action) {
case 'list-subscriptions':
return await listSubscriptions(input as ListSubscriptionsInput)
case 'get-subscription':
return await getSubscription(input as never)
case 'search-subscriptions':
return await searchSubscriptions(input as never)
case 'create-subscription':
return await createSubscription(input as CreateSubscriptionInput)
case 'cancel-subscription':
return await cancelSubscription(input as CancelSubscriptionInput)
default:
throw new Error('Unknown subscriptions action: ' + action)
}
}