Skip to content

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

Package listing

@kody/stripe

src/invoices.ts

269 lines · 7.8 KB · TypeScript
import {
	formatStripeAmount,
	idOf,
	mutationPreview,
	parseAction,
	stripeDate,
	stripeList,
	stripeRequest,
	stripeSearch,
	type StripeAuthOptions,
} from './stripe-core.ts'

export function summarizeInvoice(invoice: any) {
	if (!invoice || typeof invoice !== 'object') return null
	return {
		id: invoice.id ?? null,
		number: invoice.number ?? null,
		status: invoice.status ?? null,
		customerId: idOf(invoice.customer),
		customerEmail: invoice.customer_email ?? null,
		customerName: invoice.customer_name ?? null,
		currency: invoice.currency ?? null,
		amountDue: invoice.amount_due ?? null,
		amountPaid: invoice.amount_paid ?? null,
		total: invoice.total ?? null,
		display: formatStripeAmount(invoice.total, invoice.currency),
		created: stripeDate(invoice.created),
		dueDate: stripeDate(invoice.due_date),
		hostedInvoiceUrl: invoice.hosted_invoice_url ?? null,
		invoicePdf: invoice.invoice_pdf ?? null,
		description: invoice.description ?? null,
		lineCount: invoice.lines?.total_count ?? null,
	}
}

export type ListInvoicesInput = StripeAuthOptions & {
	customerId?: string
	status?: 'draft' | 'open' | 'paid' | 'uncollectible' | 'void'
	maxItems?: number
}

/** List invoices, optionally filtered by customer and status. */
export async function listInvoices(input: ListInvoicesInput = {}) {
	const { items, hasMore } = await stripeList('invoices', {
		account: input.account,
		secretName: input.secretName,
		maxItems: input.maxItems ?? 25,
		query: { customer: input.customerId, status: input.status },
	})
	return { hasMore, invoices: items.map(summarizeInvoice) }
}

/** Get one invoice by id (full Stripe object, lines included). */
export async function getInvoice(input: StripeAuthOptions & { invoiceId: string }) {
	return await stripeRequest({
		path: 'invoices/' + input.invoiceId,
		account: input.account,
		secretName: input.secretName,
	})
}

/**
 * Search invoices with Stripe's search query language.
 * @example searchInvoices({ searchQuery: "total>500 AND status:'paid'" })
 */
export async function searchInvoices(
	input: StripeAuthOptions & { searchQuery: string; maxItems?: number },
) {
	const { items, hasMore } = await stripeSearch('invoices/search', {
		account: input.account,
		secretName: input.secretName,
		searchQuery: input.searchQuery,
		maxItems: input.maxItems ?? 25,
	})
	return { hasMore, invoices: items.map(summarizeInvoice) }
}

export type CreateInvoiceInput = StripeAuthOptions & {
	customerId: string
	description?: string
	/** 'send_invoice' emails the customer a hosted invoice; 'charge_automatically' uses the default payment method. */
	collectionMethod?: 'charge_automatically' | 'send_invoice'
	/** Required when collectionMethod is 'send_invoice'. */
	daysUntilDue?: number
	metadata?: Record<string, string>
	/** Line items to add before returning the draft. */
	lines?: Array<{
		description: string
		/** Amount in smallest currency unit, e.g. cents. */
		amount: number
		currency?: string
		quantity?: number
	}>
	idempotencyKey?: string
	dryRun?: boolean
}

/** Create a draft invoice with optional line items. Finalize/send separately. */
export async function createInvoice(input: CreateInvoiceInput) {
	const collectionMethod = input.collectionMethod ?? 'send_invoice'
	const body = {
		customer: input.customerId,
		description: input.description,
		collection_method: collectionMethod,
		days_until_due: collectionMethod === 'send_invoice' ? (input.daysUntilDue ?? 30) : undefined,
		metadata: input.metadata,
		auto_advance: false,
	}
	const preview = mutationPreview(input, {
		action: 'create invoice',
		method: 'POST',
		path: 'invoices',
		body: { ...body, lines: input.lines },
	})
	if (preview) return preview
	const invoice = await stripeRequest({
		path: 'invoices',
		method: 'POST',
		account: input.account,
		secretName: input.secretName,
		idempotencyKey: input.idempotencyKey,
		body,
	})
	for (const line of input.lines ?? []) {
		await stripeRequest({
			path: 'invoiceitems',
			method: 'POST',
			account: input.account,
			secretName: input.secretName,
			body: {
				customer: input.customerId,
				invoice: invoice.id,
				description: line.description,
				unit_amount: line.amount,
				currency: line.currency ?? 'usd',
				quantity: line.quantity ?? 1,
			},
		})
	}
	return summarizeInvoice(
		await stripeRequest({
			path: 'invoices/' + invoice.id,
			account: input.account,
			secretName: input.secretName,
		}),
	)
}

/** Finalize a draft invoice so it can be sent or paid. Requires confirm: true. */
export async function finalizeInvoice(
	input: StripeAuthOptions & { invoiceId: string; confirm?: boolean; dryRun?: boolean },
) {
	const preview = mutationPreview(input, {
		action: 'finalize invoice ' + input.invoiceId,
		method: 'POST',
		path: 'invoices/' + input.invoiceId + '/finalize',
		requireConfirm: true,
	})
	if (preview) return preview
	return summarizeInvoice(
		await stripeRequest({
			path: 'invoices/' + input.invoiceId + '/finalize',
			method: 'POST',
			account: input.account,
			secretName: input.secretName,
		}),
	)
}

/** Email a finalized invoice to the customer. Requires confirm: true. */
export async function sendInvoice(
	input: StripeAuthOptions & { invoiceId: string; confirm?: boolean; dryRun?: boolean },
) {
	const preview = mutationPreview(input, {
		action: 'send invoice ' + input.invoiceId,
		method: 'POST',
		path: 'invoices/' + input.invoiceId + '/send',
		requireConfirm: true,
	})
	if (preview) return preview
	return summarizeInvoice(
		await stripeRequest({
			path: 'invoices/' + input.invoiceId + '/send',
			method: 'POST',
			account: input.account,
			secretName: input.secretName,
		}),
	)
}

/** Void a finalized invoice (irreversible). Requires confirm: true. */
export async function voidInvoice(
	input: StripeAuthOptions & { invoiceId: string; confirm?: boolean; dryRun?: boolean },
) {
	const preview = mutationPreview(input, {
		action: 'void invoice ' + input.invoiceId,
		method: 'POST',
		path: 'invoices/' + input.invoiceId + '/void',
		requireConfirm: true,
	})
	if (preview) return preview
	return summarizeInvoice(
		await stripeRequest({
			path: 'invoices/' + input.invoiceId + '/void',
			method: 'POST',
			account: input.account,
			secretName: input.secretName,
		}),
	)
}

/** Delete a draft invoice (drafts only). Requires confirm: true. */
export async function deleteDraftInvoice(
	input: StripeAuthOptions & { invoiceId: string; confirm?: boolean; dryRun?: boolean },
) {
	const preview = mutationPreview(input, {
		action: 'delete draft invoice ' + input.invoiceId,
		method: 'DELETE',
		path: 'invoices/' + input.invoiceId,
		requireConfirm: true,
	})
	if (preview) return preview
	return await stripeRequest({
		path: 'invoices/' + input.invoiceId,
		method: 'DELETE',
		account: input.account,
		secretName: input.secretName,
	})
}

const invoiceActions = [
	'list-invoices',
	'get-invoice',
	'search-invoices',
	'create-invoice',
	'finalize-invoice',
	'send-invoice',
	'void-invoice',
	'delete-draft-invoice',
] as const

/**
 * Invoices dispatcher. Defaults to list-invoices.
 */
export default async function invoices(input: Record<string, unknown> = {}) {
	const action = parseAction(input.action, invoiceActions, 'list-invoices', 'invoices')
	switch (action) {
		case 'list-invoices':
			return await listInvoices(input as ListInvoicesInput)
		case 'get-invoice':
			return await getInvoice(input as never)
		case 'search-invoices':
			return await searchInvoices(input as never)
		case 'create-invoice':
			return await createInvoice(input as CreateInvoiceInput)
		case 'finalize-invoice':
			return await finalizeInvoice(input as never)
		case 'send-invoice':
			return await sendInvoice(input as never)
		case 'void-invoice':
			return await voidInvoice(input as never)
		case 'delete-draft-invoice':
			return await deleteDraftInvoice(input as never)
		default: {
			const exhaustive: never = action
			throw new Error('Unhandled invoices action: ' + String(exhaustive))
		}
	}
}