Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/stripe

src/products.ts

305 lines · 8.5 KB · TypeScript
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))
		}
	}
}