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))
}
}
}