Skip to content

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

Package listing

@kody/plaid

src/transactions.ts

123 lines · 3.6 KB · TypeScript
import {
	accessTokenForPreview,
	isoDate,
	mutationPreview,
	normalizeAccount,
	normalizeTransaction,
	plaidRequest,
	type MutationGuardInput,
	type PlaidAuthInput,
} from './plaid-core.ts'

export type SyncTransactionsInput = PlaidAuthInput & {
	cursor?: string
	count?: number
}

/**
 * Incremental transaction updates (`/transactions/sync`). Pass `cursor` from
 * the previous `next_cursor` to page.
 */
export async function syncTransactions(input: SyncTransactionsInput = {}) {
	const count = input.count == null ? 100 : Number(input.count)
	if (!Number.isFinite(count) || count < 1 || count > 500) {
		throw new Error('count must be between 1 and 500.')
	}
	const data = await plaidRequest({
		...input,
		path: '/transactions/sync',
		includeAccessToken: true,
		body: {
			...(input.cursor ? { cursor: input.cursor } : {}),
			count,
		},
	})
	const added = Array.isArray(data?.added) ? data.added.map(normalizeTransaction) : []
	const modified = Array.isArray(data?.modified) ? data.modified.map(normalizeTransaction) : []
	const removed = Array.isArray(data?.removed)
		? data.removed.map((entry: any) => ({ transaction_id: entry?.transaction_id ?? null }))
		: []
	return {
		added,
		modified,
		removed,
		next_cursor: data?.next_cursor ?? null,
		has_more: Boolean(data?.has_more),
		accounts: Array.isArray(data?.accounts) ? data.accounts.map(normalizeAccount) : [],
		request_id: data?.request_id ?? null,
	}
}

export type ListTransactionsInput = PlaidAuthInput & {
	startDate?: string | Date
	endDate?: string | Date
	accountIds?: string[]
	count?: number
	offset?: number
}

/**
 * Date-window transactions (`/transactions/get`). Prefer `syncTransactions`
 * for ongoing updates.
 */
export async function listTransactions(input: ListTransactionsInput = {}) {
	const startDate = isoDate(input.startDate, 'startDate')
	const endDate = isoDate(input.endDate, 'endDate')
	if (!startDate || !endDate) {
		throw new Error('listTransactions requires startDate and endDate (YYYY-MM-DD).')
	}
	const count = input.count == null ? 100 : Number(input.count)
	if (!Number.isFinite(count) || count < 1 || count > 500) {
		throw new Error('count must be between 1 and 500.')
	}
	const offset = input.offset == null ? 0 : Number(input.offset)
	if (!Number.isFinite(offset) || offset < 0) {
		throw new Error('offset must be a non-negative number.')
	}
	const data = await plaidRequest({
		...input,
		path: '/transactions/get',
		includeAccessToken: true,
		body: {
			start_date: startDate,
			end_date: endDate,
			options: {
				count,
				offset,
				...(input.accountIds?.length ? { account_ids: input.accountIds } : {}),
			},
		},
	})
	return {
		transactions: Array.isArray(data?.transactions) ? data.transactions.map(normalizeTransaction) : [],
		accounts: Array.isArray(data?.accounts) ? data.accounts.map(normalizeAccount) : [],
		total_transactions: typeof data?.total_transactions === 'number' ? data.total_transactions : null,
		request_id: data?.request_id ?? null,
	}
}

export type RefreshTransactionsInput = MutationGuardInput

/**
 * Ask Plaid to check for new transactions (`/transactions/refresh`).
 * Defaults to dry-run; live refresh needs `confirm: true`.
 */
export async function refreshTransactions(input: RefreshTransactionsInput = {}) {
	const preview = mutationPreview(input, {
		action: 'refresh transactions',
		method: 'POST',
		path: '/transactions/refresh',
		body: { access_token: accessTokenForPreview(input) },
	})
	if (preview) return preview
	const data = await plaidRequest({
		...input,
		path: '/transactions/refresh',
		includeAccessToken: true,
	})
	return {
		request_id: data?.request_id ?? null,
	}
}

export default syncTransactions