Skip to content

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

Package listing

@kody/stripe

src/payments.ts

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

export function summarizePaymentIntent(intent: any) {
	if (!intent || typeof intent !== 'object') return null
	return {
		id: intent.id ?? null,
		status: intent.status ?? null,
		amount: intent.amount ?? null,
		currency: intent.currency ?? null,
		display: formatStripeAmount(intent.amount, intent.currency),
		customerId: idOf(intent.customer),
		description: intent.description ?? null,
		created: stripeDate(intent.created),
		latestChargeId: idOf(intent.latest_charge),
		receiptEmail: intent.receipt_email ?? null,
		metadata: intent.metadata ?? {},
	}
}

export function summarizeCharge(charge: any) {
	if (!charge || typeof charge !== 'object') return null
	return {
		id: charge.id ?? null,
		status: charge.status ?? null,
		paid: Boolean(charge.paid),
		refunded: Boolean(charge.refunded),
		amount: charge.amount ?? null,
		amountRefunded: charge.amount_refunded ?? null,
		currency: charge.currency ?? null,
		display: formatStripeAmount(charge.amount, charge.currency),
		customerId: idOf(charge.customer),
		paymentIntentId: idOf(charge.payment_intent),
		description: charge.description ?? null,
		receiptEmail: charge.receipt_email ?? charge.billing_details?.email ?? null,
		billingName: charge.billing_details?.name ?? null,
		created: stripeDate(charge.created),
		receiptUrl: charge.receipt_url ?? null,
	}
}

export type ListPaymentIntentsInput = StripeAuthOptions & {
	customerId?: string
	maxItems?: number
}

/** List payment intents, optionally for one customer. */
export async function listPaymentIntents(input: ListPaymentIntentsInput = {}) {
	const { items, hasMore } = await stripeList('payment_intents', {
		account: input.account,
		secretName: input.secretName,
		maxItems: input.maxItems ?? 25,
		query: { customer: input.customerId },
	})
	return { hasMore, paymentIntents: items.map(summarizePaymentIntent) }
}

/** Get one payment intent by id (full Stripe object). */
export async function getPaymentIntent(input: StripeAuthOptions & { paymentIntentId: string }) {
	return await stripeRequest({
		path: 'payment_intents/' + input.paymentIntentId,
		account: input.account,
		secretName: input.secretName,
	})
}

/**
 * Search payment intents with Stripe's search query language.
 * @example searchPaymentIntents({ searchQuery: "status:'succeeded' AND amount>1000" })
 */
export async function searchPaymentIntents(
	input: StripeAuthOptions & { searchQuery: string; maxItems?: number },
) {
	const { items, hasMore } = await stripeSearch('payment_intents/search', {
		account: input.account,
		secretName: input.secretName,
		searchQuery: input.searchQuery,
		maxItems: input.maxItems ?? 25,
	})
	return { hasMore, paymentIntents: items.map(summarizePaymentIntent) }
}

export type ListChargesInput = StripeAuthOptions & {
	customerId?: string
	paymentIntentId?: string
	maxItems?: number
}

/** List charges, optionally scoped to a customer or payment intent. */
export async function listCharges(input: ListChargesInput = {}) {
	const { items, hasMore } = await stripeList('charges', {
		account: input.account,
		secretName: input.secretName,
		maxItems: input.maxItems ?? 25,
		query: { customer: input.customerId, payment_intent: input.paymentIntentId },
	})
	return { hasMore, charges: items.map(summarizeCharge) }
}

/** Get one charge by id (full Stripe object). */
export async function getCharge(input: StripeAuthOptions & { chargeId: string }) {
	return await stripeRequest({
		path: 'charges/' + input.chargeId,
		account: input.account,
		secretName: input.secretName,
	})
}

/**
 * Search charges with Stripe's search query language.
 * @example searchCharges({ searchQuery: "billing_details.email:'ada@example.com'" })
 */
export async function searchCharges(
	input: StripeAuthOptions & { searchQuery: string; maxItems?: number },
) {
	const { items, hasMore } = await stripeSearch('charges/search', {
		account: input.account,
		secretName: input.secretName,
		searchQuery: input.searchQuery,
		maxItems: input.maxItems ?? 25,
	})
	return { hasMore, charges: items.map(summarizeCharge) }
}

export type ListRefundsInput = StripeAuthOptions & {
	chargeId?: string
	paymentIntentId?: string
	maxItems?: number
}

/** List refunds, optionally scoped to a charge or payment intent. */
export async function listRefunds(input: ListRefundsInput = {}) {
	const { items, hasMore } = await stripeList('refunds', {
		account: input.account,
		secretName: input.secretName,
		maxItems: input.maxItems ?? 25,
		query: { charge: input.chargeId, payment_intent: input.paymentIntentId },
	})
	return {
		hasMore,
		refunds: items.map((refund) => ({
			id: refund.id,
			status: refund.status,
			amount: refund.amount,
			currency: refund.currency,
			display: formatStripeAmount(refund.amount, refund.currency),
			chargeId: idOf(refund.charge),
			paymentIntentId: idOf(refund.payment_intent),
			reason: refund.reason ?? null,
			created: stripeDate(refund.created),
		})),
	}
}

export type CreateRefundInput = StripeAuthOptions & {
	chargeId?: string
	paymentIntentId?: string
	/** Amount in smallest currency unit; omit for a full refund. */
	amount?: number
	reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer'
	idempotencyKey?: string
	/** Refunds move real money; must be true unless dryRun. */
	confirm?: boolean
	dryRun?: boolean
}

/** Create a refund (full or partial). Requires confirm: true. Pass dryRun: true to preview. */
export async function createRefund(input: CreateRefundInput) {
	if (!input.chargeId && !input.paymentIntentId) {
		throw new Error('Pass chargeId or paymentIntentId to createRefund.')
	}
	const body = {
		charge: input.chargeId,
		payment_intent: input.paymentIntentId,
		amount: input.amount,
		reason: input.reason,
	}
	const preview = mutationPreview(input, {
		action: 'refund ' + (input.chargeId ?? input.paymentIntentId),
		method: 'POST',
		path: 'refunds',
		body,
		requireConfirm: true,
	})
	if (preview) return preview
	return await stripeRequest({
		path: 'refunds',
		method: 'POST',
		account: input.account,
		secretName: input.secretName,
		idempotencyKey: input.idempotencyKey,
		body,
	})
}

const paymentActions = [
	'list-payment-intents',
	'get-payment-intent',
	'search-payment-intents',
	'list-charges',
	'get-charge',
	'search-charges',
	'list-refunds',
	'create-refund',
] as const

/**
 * Payments dispatcher. Defaults to list-charges.
 */
export default async function payments(input: Record<string, unknown> = {}) {
	const action = parseAction(input.action, paymentActions, 'list-charges', 'payments')
	switch (action) {
		case 'list-payment-intents':
			return await listPaymentIntents(input as ListPaymentIntentsInput)
		case 'get-payment-intent':
			return await getPaymentIntent(input as never)
		case 'search-payment-intents':
			return await searchPaymentIntents(input as never)
		case 'list-charges':
			return await listCharges(input as ListChargesInput)
		case 'get-charge':
			return await getCharge(input as never)
		case 'search-charges':
			return await searchCharges(input as never)
		case 'list-refunds':
			return await listRefunds(input as ListRefundsInput)
		case 'create-refund':
			return await createRefund(input as CreateRefundInput)
		default: {
			const exhaustive: never = action
			throw new Error('Unhandled payments action: ' + String(exhaustive))
		}
	}
}