import {
formatStripeAmount,
idOf,
mutationPreview,
parseAction,
stripeDate,
stripeList,
stripeRequest,
stripeSearch,
type StripeAuthOptions,
} 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 = StripeAuthOptions & {
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', {
account: input.account,
secretName: input.secretName,
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: StripeAuthOptions & { subscriptionId: string }) {
return await stripeRequest({
path: 'subscriptions/' + input.subscriptionId,
account: input.account,
secretName: input.secretName,
})
}
/**
* Search subscriptions with Stripe's search query language.
* @example searchSubscriptions({ searchQuery: "status:'active' AND metadata['plan']:'pro'" })
*/
export async function searchSubscriptions(
input: StripeAuthOptions & { searchQuery: string; maxItems?: number },
) {
const { items, hasMore } = await stripeSearch('subscriptions/search', {
account: input.account,
secretName: input.secretName,
searchQuery: input.searchQuery,
maxItems: input.maxItems ?? 25,
})
return { hasMore, subscriptions: items.map(summarizeSubscription) }
}
export type CreateSubscriptionInput = StripeAuthOptions & {
customerId: string
priceId: string
quantity?: number
trialDays?: number
metadata?: Record<string, string>
idempotencyKey?: string
/** Subscriptions bill real money; must be true unless dryRun. */
confirm?: boolean
dryRun?: boolean
}
/** Create a subscription for an existing customer + price. Requires confirm: true. */
export async function createSubscription(input: CreateSubscriptionInput) {
const body = {
customer: input.customerId,
items: [{ price: input.priceId, quantity: input.quantity ?? 1 }],
trial_period_days: input.trialDays,
metadata: input.metadata,
}
const preview = mutationPreview(input, {
action: 'create a subscription for ' + input.customerId,
method: 'POST',
path: 'subscriptions',
body,
requireConfirm: true,
})
if (preview) return preview
const subscription = await stripeRequest({
path: 'subscriptions',
method: 'POST',
account: input.account,
secretName: input.secretName,
idempotencyKey: input.idempotencyKey,
body,
})
return summarizeSubscription(subscription)
}
export type CancelSubscriptionInput = StripeAuthOptions & {
subscriptionId: string
/** Default true: cancel at period end instead of immediately. */
atPeriodEnd?: boolean
confirm?: boolean
dryRun?: boolean
}
/** Cancel a subscription (at period end by default). Requires confirm: true. */
export async function cancelSubscription(input: CancelSubscriptionInput) {
const atPeriodEnd = input.atPeriodEnd !== false
const preview = mutationPreview(input, {
action: 'cancel subscription ' + input.subscriptionId,
method: atPeriodEnd ? 'POST' : 'DELETE',
path: 'subscriptions/' + input.subscriptionId,
body: atPeriodEnd ? { cancel_at_period_end: true } : undefined,
requireConfirm: true,
})
if (preview) return preview
if (!atPeriodEnd) {
return summarizeSubscription(
await stripeRequest({
path: 'subscriptions/' + input.subscriptionId,
method: 'DELETE',
account: input.account,
secretName: input.secretName,
}),
)
}
return summarizeSubscription(
await stripeRequest({
path: 'subscriptions/' + input.subscriptionId,
method: 'POST',
account: input.account,
secretName: input.secretName,
body: { cancel_at_period_end: true },
}),
)
}
const subscriptionActions = [
'list-subscriptions',
'get-subscription',
'search-subscriptions',
'create-subscription',
'cancel-subscription',
] as const
/**
* Subscriptions dispatcher. Defaults to list-subscriptions.
*/
export default async function subscriptions(input: Record<string, unknown> = {}) {
const action = parseAction(
input.action,
subscriptionActions,
'list-subscriptions',
'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: {
const exhaustive: never = action
throw new Error('Unhandled subscriptions action: ' + String(exhaustive))
}
}
}