Skip to content

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

Package listing

@kody/stripe

src/account.ts

181 lines · 5.3 KB · TypeScript
import {
	formatStripeAmount,
	parseAction,
	stripeDate,
	stripeList,
	stripeRequest,
	type StripeAuthOptions,
} from './stripe-core.ts'

/**
 * Get the Stripe account profile for the authenticated key.
 * @example
 * import { getAccount } from 'kody:@kody/stripe/account'
 * const account = await getAccount()
 */
export async function getAccount(input: StripeAuthOptions = {}) {
	const account = await stripeRequest({ path: 'account', ...input })
	return {
		id: account.id ?? null,
		displayName: account.settings?.dashboard?.display_name ?? account.business_profile?.name ?? null,
		email: account.email ?? null,
		country: account.country ?? null,
		defaultCurrency: account.default_currency ?? null,
		chargesEnabled: Boolean(account.charges_enabled),
		payoutsEnabled: Boolean(account.payouts_enabled),
		livemode: Boolean(account.livemode),
	}
}

/** Get available and pending balance amounts. */
export async function getBalance(input: StripeAuthOptions = {}) {
	const balance = await stripeRequest({ path: 'balance', ...input })
	const shape = (entries: any[]) =>
		(entries ?? []).map((entry) => ({
			amount: entry.amount,
			currency: entry.currency,
			display: formatStripeAmount(entry.amount, entry.currency),
		}))
	return {
		livemode: Boolean(balance.livemode),
		available: shape(balance.available),
		pending: shape(balance.pending),
	}
}

export type ListBalanceTransactionsInput = StripeAuthOptions & {
	/** Filter by type, e.g. 'charge', 'refund', 'payout'. */
	type?: string
	/** Created window: ISO strings, Date, or unix seconds. */
	createdGte?: string | number | Date
	createdLte?: string | number | Date
	maxItems?: number
}

export function toUnixSeconds(value: string | number | Date | undefined) {
	if (value == null) return undefined
	if (typeof value === 'number') return value
	const date = value instanceof Date ? value : new Date(value)
	if (Number.isNaN(date.getTime())) throw new Error('Invalid date: ' + String(value))
	return Math.floor(date.getTime() / 1000)
}

/** List balance transactions (money movements) with compact summaries. */
export async function listBalanceTransactions(input: ListBalanceTransactionsInput = {}) {
	const { items, hasMore } = await stripeList('balance_transactions', {
		account: input.account,
		secretName: input.secretName,
		maxItems: input.maxItems ?? 25,
		query: {
			type: input.type,
			created: {
				gte: toUnixSeconds(input.createdGte),
				lte: toUnixSeconds(input.createdLte),
			},
		},
	})
	return {
		hasMore,
		transactions: items.map((txn) => ({
			id: txn.id,
			type: txn.type,
			status: txn.status,
			amount: txn.amount,
			fee: txn.fee,
			net: txn.net,
			currency: txn.currency,
			display: formatStripeAmount(txn.amount, txn.currency),
			description: txn.description ?? null,
			created: stripeDate(txn.created),
			availableOn: stripeDate(txn.available_on),
		})),
	}
}

export type ListPayoutsInput = StripeAuthOptions & {
	status?: 'pending' | 'paid' | 'failed' | 'canceled' | 'in_transit'
	maxItems?: number
}

/** List payouts to the connected bank account. */
export async function listPayouts(input: ListPayoutsInput = {}) {
	const { items, hasMore } = await stripeList('payouts', {
		account: input.account,
		secretName: input.secretName,
		maxItems: input.maxItems ?? 25,
		query: { status: input.status },
	})
	return {
		hasMore,
		payouts: items.map((payout) => ({
			id: payout.id,
			status: payout.status,
			amount: payout.amount,
			currency: payout.currency,
			display: formatStripeAmount(payout.amount, payout.currency),
			arrivalDate: stripeDate(payout.arrival_date),
			created: stripeDate(payout.created),
			method: payout.method ?? null,
			description: payout.description ?? null,
		})),
	}
}

export type ListEventsInput = StripeAuthOptions & {
	/** Event type filter, e.g. 'invoice.paid' or 'charge.*'. */
	type?: string
	maxItems?: number
}

/** List recent Stripe events (useful for debugging webhooks and activity). */
export async function listEvents(input: ListEventsInput = {}) {
	const { items, hasMore } = await stripeList('events', {
		account: input.account,
		secretName: input.secretName,
		maxItems: input.maxItems ?? 25,
		query: { type: input.type },
	})
	return {
		hasMore,
		events: items.map((event) => ({
			id: event.id,
			type: event.type,
			created: stripeDate(event.created),
			livemode: Boolean(event.livemode),
			objectId: event.data?.object?.id ?? null,
			objectType: event.data?.object?.object ?? null,
		})),
	}
}

const accountActions = [
	'get-account',
	'get-balance',
	'list-balance-transactions',
	'list-payouts',
	'list-events',
] as const

/**
 * Account/balance dispatcher. Defaults to get-account.
 * Actions: get-account, get-balance, list-balance-transactions, list-payouts, list-events.
 */
export default async function account(input: Record<string, unknown> = {}) {
	const action = parseAction(input.action, accountActions, 'get-account', 'account')
	switch (action) {
		case 'get-account':
			return await getAccount(input)
		case 'get-balance':
			return await getBalance(input)
		case 'list-balance-transactions':
			return await listBalanceTransactions(input as ListBalanceTransactionsInput)
		case 'list-payouts':
			return await listPayouts(input as ListPayoutsInput)
		case 'list-events':
			return await listEvents(input as ListEventsInput)
		default: {
			const exhaustive: never = action
			throw new Error('Unhandled account action: ' + String(exhaustive))
		}
	}
}