import {
formatStripeAmount,
idOf,
mutationPreview,
parseAction,
stripeDate,
stripeList,
stripeRequest,
type StripeAuthOptions,
} from './stripe-core.ts'
export function summarizeProduct(product: any) {
if (!product || typeof product !== 'object') return null
return {
id: product.id ?? null,
name: product.name ?? null,
active: Boolean(product.active),
description: product.description ?? null,
defaultPriceId: idOf(product.default_price),
created: stripeDate(product.created),
metadata: product.metadata ?? {},
}
}
export function summarizePrice(price: any) {
if (!price || typeof price !== 'object') return null
return {
id: price.id ?? null,
productId: idOf(price.product),
active: Boolean(price.active),
nickname: price.nickname ?? null,
unitAmount: price.unit_amount ?? null,
currency: price.currency ?? null,
display: formatStripeAmount(price.unit_amount, price.currency),
type: price.type ?? null,
interval: price.recurring?.interval ?? null,
intervalCount: price.recurring?.interval_count ?? null,
created: stripeDate(price.created),
}
}
/** List products (active only by default). */
export async function listProducts(
input: StripeAuthOptions & { active?: boolean; maxItems?: number } = {},
) {
const { items, hasMore } = await stripeList('products', {
account: input.account,
secretName: input.secretName,
maxItems: input.maxItems ?? 25,
query: { active: input.active ?? true },
})
return { hasMore, products: items.map(summarizeProduct) }
}
/** Get one product by id (full Stripe object). */
export async function getProduct(input: StripeAuthOptions & { productId: string }) {
return await stripeRequest({
path: 'products/' + input.productId,
account: input.account,
secretName: input.secretName,
})
}
/** List prices, optionally for one product. */
export async function listPrices(
input: StripeAuthOptions & { productId?: string; active?: boolean; maxItems?: number } = {},
) {
const { items, hasMore } = await stripeList('prices', {
account: input.account,
secretName: input.secretName,
maxItems: input.maxItems ?? 25,
query: { product: input.productId, active: input.active ?? true },
})
return { hasMore, prices: items.map(summarizePrice) }
}
export type CreateProductInput = StripeAuthOptions & {
name: string
description?: string
metadata?: Record<string, string>
/** Create a default price alongside the product. */
price?: {
/** Amount in smallest currency unit, e.g. cents. */
amount: number
currency?: string
/** Omit for a one-time price. */
interval?: 'day' | 'week' | 'month' | 'year'
}
idempotencyKey?: string
dryRun?: boolean
}
/** Create a product, optionally with a default price. Pass dryRun: true to preview. */
export async function createProduct(input: CreateProductInput) {
const body = {
name: input.name,
description: input.description,
metadata: input.metadata,
default_price_data: input.price
? {
unit_amount: input.price.amount,
currency: input.price.currency ?? 'usd',
recurring: input.price.interval ? { interval: input.price.interval } : undefined,
}
: undefined,
}
const preview = mutationPreview(input, {
action: 'create product',
method: 'POST',
path: 'products',
body,
})
if (preview) return preview
const product = await stripeRequest({
path: 'products',
method: 'POST',
account: input.account,
secretName: input.secretName,
idempotencyKey: input.idempotencyKey,
body,
})
return summarizeProduct(product)
}
export type CreatePriceInput = StripeAuthOptions & {
productId: string
/** Amount in smallest currency unit, e.g. cents. */
amount: number
currency?: string
/** Omit for a one-time price. */
interval?: 'day' | 'week' | 'month' | 'year'
nickname?: string
idempotencyKey?: string
dryRun?: boolean
}
/** Create a price for an existing product. Pass dryRun: true to preview. */
export async function createPrice(input: CreatePriceInput) {
const body = {
product: input.productId,
unit_amount: input.amount,
currency: input.currency ?? 'usd',
recurring: input.interval ? { interval: input.interval } : undefined,
nickname: input.nickname,
}
const preview = mutationPreview(input, {
action: 'create price',
method: 'POST',
path: 'prices',
body,
})
if (preview) return preview
const price = await stripeRequest({
path: 'prices',
method: 'POST',
account: input.account,
secretName: input.secretName,
idempotencyKey: input.idempotencyKey,
body,
})
return summarizePrice(price)
}
/** Archive (deactivate) a product. Requires confirm: true. */
export async function archiveProduct(
input: StripeAuthOptions & { productId: string; confirm?: boolean; dryRun?: boolean },
) {
const preview = mutationPreview(input, {
action: 'archive product ' + input.productId,
method: 'POST',
path: 'products/' + input.productId,
body: { active: false },
requireConfirm: true,
})
if (preview) return preview
const product = await stripeRequest({
path: 'products/' + input.productId,
method: 'POST',
account: input.account,
secretName: input.secretName,
body: { active: false },
})
return summarizeProduct(product)
}
export function summarizePaymentLink(link: any) {
if (!link || typeof link !== 'object') return null
return {
id: link.id ?? null,
url: link.url ?? null,
active: Boolean(link.active),
currency: link.currency ?? null,
metadata: link.metadata ?? {},
}
}
/** List payment links. */
export async function listPaymentLinks(
input: StripeAuthOptions & { active?: boolean; maxItems?: number } = {},
) {
const { items, hasMore } = await stripeList('payment_links', {
account: input.account,
secretName: input.secretName,
maxItems: input.maxItems ?? 25,
query: { active: input.active },
})
return { hasMore, paymentLinks: items.map(summarizePaymentLink) }
}
export type CreatePaymentLinkInput = StripeAuthOptions & {
priceId: string
quantity?: number
metadata?: Record<string, string>
idempotencyKey?: string
dryRun?: boolean
}
/** Create a shareable payment link for an existing price. Pass dryRun: true to preview. */
export async function createPaymentLink(input: CreatePaymentLinkInput) {
const body = {
line_items: [{ price: input.priceId, quantity: input.quantity ?? 1 }],
metadata: input.metadata,
}
const preview = mutationPreview(input, {
action: 'create payment link',
method: 'POST',
path: 'payment_links',
body,
})
if (preview) return preview
const link = await stripeRequest({
path: 'payment_links',
method: 'POST',
account: input.account,
secretName: input.secretName,
idempotencyKey: input.idempotencyKey,
body,
})
return summarizePaymentLink(link)
}
/** Deactivate a payment link (links cannot be deleted). Requires confirm: true. */
export async function deactivatePaymentLink(
input: StripeAuthOptions & { paymentLinkId: string; confirm?: boolean; dryRun?: boolean },
) {
const preview = mutationPreview(input, {
action: 'deactivate payment link ' + input.paymentLinkId,
method: 'POST',
path: 'payment_links/' + input.paymentLinkId,
body: { active: false },
requireConfirm: true,
})
if (preview) return preview
const link = await stripeRequest({
path: 'payment_links/' + input.paymentLinkId,
method: 'POST',
account: input.account,
secretName: input.secretName,
body: { active: false },
})
return summarizePaymentLink(link)
}
const productActions = [
'list-products',
'get-product',
'create-product',
'archive-product',
'list-prices',
'create-price',
'list-payment-links',
'create-payment-link',
'deactivate-payment-link',
] as const
/**
* Products/prices/payment-links dispatcher. Defaults to list-products.
*/
export default async function products(input: Record<string, unknown> = {}) {
const action = parseAction(input.action, productActions, 'list-products', 'products')
switch (action) {
case 'list-products':
return await listProducts(input as never)
case 'get-product':
return await getProduct(input as never)
case 'create-product':
return await createProduct(input as CreateProductInput)
case 'archive-product':
return await archiveProduct(input as never)
case 'list-prices':
return await listPrices(input as never)
case 'create-price':
return await createPrice(input as CreatePriceInput)
case 'list-payment-links':
return await listPaymentLinks(input as never)
case 'create-payment-link':
return await createPaymentLink(input as CreatePaymentLinkInput)
case 'deactivate-payment-link':
return await deactivatePaymentLink(input as never)
default: {
const exhaustive: never = action
throw new Error('Unhandled products action: ' + String(exhaustive))
}
}
}