Skip to content
← Public packages

@kentcdodds/stripe

Stripe helpers for customers, payments, invoices, subscriptions, products, payment links, refunds, and balance.

src/products.ts

223 lines · 6.7 KB · TypeScript
import {
	formatStripeAmount,
	idOf,
	requireConfirm,
	stripeDate,
	stripeList,
	stripeRequest,
} 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: { active?: boolean; maxItems?: number } = {}) {
	const { items, hasMore } = await stripeList('products', {
		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: { productId: string }) {
	return await stripeRequest({ path: 'products/' + input.productId })
}

/** List prices, optionally for one product. */
export async function listPrices(
	input: { productId?: string; active?: boolean; maxItems?: number } = {},
) {
	const { items, hasMore } = await stripeList('prices', {
		maxItems: input.maxItems ?? 25,
		query: { product: input.productId, active: input.active ?? true },
	})
	return { hasMore, prices: items.map(summarizePrice) }
}

export type CreateProductInput = {
	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
}

/** Create a product, optionally with a default price. */
export async function createProduct(input: CreateProductInput) {
	const product = await stripeRequest({
		path: 'products',
		method: 'POST',
		idempotencyKey: input.idempotencyKey,
		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,
		},
	})
	return summarizeProduct(product)
}

export type CreatePriceInput = {
	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
}

/** Create a price for an existing product. */
export async function createPrice(input: CreatePriceInput) {
	const price = await stripeRequest({
		path: 'prices',
		method: 'POST',
		idempotencyKey: input.idempotencyKey,
		body: {
			product: input.productId,
			unit_amount: input.amount,
			currency: input.currency ?? 'usd',
			recurring: input.interval ? { interval: input.interval } : undefined,
			nickname: input.nickname,
		},
	})
	return summarizePrice(price)
}

/** Archive (deactivate) a product. Requires confirm: true. */
export async function archiveProduct(input: { productId: string; confirm?: boolean }) {
	requireConfirm(input, 'archive product ' + input.productId)
	const product = await stripeRequest({
		path: 'products/' + input.productId,
		method: 'POST',
		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: { active?: boolean; maxItems?: number } = {}) {
	const { items, hasMore } = await stripeList('payment_links', {
		maxItems: input.maxItems ?? 25,
		query: { active: input.active },
	})
	return { hasMore, paymentLinks: items.map(summarizePaymentLink) }
}

export type CreatePaymentLinkInput = {
	priceId: string
	quantity?: number
	metadata?: Record<string, string>
	idempotencyKey?: string
}

/** Create a shareable payment link for an existing price. */
export async function createPaymentLink(input: CreatePaymentLinkInput) {
	const link = await stripeRequest({
		path: 'payment_links',
		method: 'POST',
		idempotencyKey: input.idempotencyKey,
		body: {
			line_items: [{ price: input.priceId, quantity: input.quantity ?? 1 }],
			metadata: input.metadata,
		},
	})
	return summarizePaymentLink(link)
}

/** Deactivate a payment link (links cannot be deleted). Requires confirm: true. */
export async function deactivatePaymentLink(input: { paymentLinkId: string; confirm?: boolean }) {
	requireConfirm(input, 'deactivate payment link ' + input.paymentLinkId)
	const link = await stripeRequest({
		path: 'payment_links/' + input.paymentLinkId,
		method: 'POST',
		body: { active: false },
	})
	return summarizePaymentLink(link)
}

/**
 * Products/prices/payment-links dispatcher. Defaults to list-products.
 * Actions: list-products, get-product, create-product, archive-product,
 * list-prices, create-price, list-payment-links, create-payment-link,
 * deactivate-payment-link.
 */
export default async function products(input: Record<string, unknown> = {}) {
	const action = String(input.action ?? 'list-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:
			throw new Error('Unknown products action: ' + action)
	}
}