import { includesText, normalizeTransactionSummary, numericMoneyValue, toPayPalDate, type PayPalAuthInput, } from './paypal-helpers.ts' import { paypalListTransactions } from './paypal-core.ts' export type ListTransactionsInput = PayPalAuthInput & { startDate: string | Date endDate: string | Date page?: number pageSize?: number fields?: string transactionId?: string transactionType?: string transactionStatus?: string includeRaw?: boolean } export type SearchTransactionsInput = PayPalAuthInput & { startDate?: string | Date endDate?: string | Date pageSize?: number maxPagesPerChunk?: number query?: string email?: string amount?: string | number currencyCode?: string invoiceId?: string transactionStatus?: string direction?: 'incoming' | 'outgoing' includeRaw?: boolean } function defaultDateRange() { const end = new Date() const start = new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000) return { start, end } } /** * List and search PayPal account transactions (reporting API). * Use for reconciliation; PayPal rejects single ranges over 31 days. * * @param input - Date range and optional filters (text, email, amount, direction). * @returns Matching transaction summaries. * * @example * import transactions from 'kody:@kody/paypal/transactions' * const rows = await transactions({ startDate: '2026-08-01', endDate: '2026-08-28' }) */ export function splitDateRange(startInput: string | Date, endInput: string | Date) { const start = startInput instanceof Date ? startInput : new Date(startInput) const end = endInput instanceof Date ? endInput : new Date(endInput) if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { throw new Error('Invalid PayPal transaction date range.') } if (start > end) { throw new Error('startDate must be before endDate.') } const chunks: Array<{ start: Date; end: Date }> = [] let cursor = new Date(start) const chunkMs = 30 * 24 * 60 * 60 * 1000 while (cursor <= end) { const chunkEnd = new Date(Math.min(end.getTime(), cursor.getTime() + chunkMs)) chunks.push({ start: new Date(cursor), end: chunkEnd }) cursor = new Date(chunkEnd.getTime() + 1000) } return chunks } export function transactionMatches(summary: any, input: SearchTransactionsInput) { if (input.email && !includesText(summary.payer_email, input.email)) return false if (input.invoiceId && !includesText(summary.invoice_id, input.invoiceId)) return false if (input.currencyCode && summary.transaction_amount?.currency_code !== input.currencyCode) return false if (input.transactionStatus && summary.transaction_status !== input.transactionStatus) return false const amount = numericMoneyValue(summary.transaction_amount) if (input.amount != null) { const expected = Number(input.amount) if (!Number.isFinite(expected) || amount == null || Math.abs(Math.abs(amount) - Math.abs(expected)) > 0.005) { return false } } if (input.direction === 'incoming' && (amount == null || amount <= 0)) return false if (input.direction === 'outgoing' && (amount == null || amount >= 0)) return false if (input.query) { const haystack = [ summary.transaction_id, summary.invoice_id, summary.transaction_subject, summary.transaction_note, summary.payer_email, summary.payer_name, summary.transaction_amount?.value, ].join(' ') if (!includesText(haystack, input.query)) return false } return true } /** * List PayPal account transactions for a single PayPal reporting date range * (OpenAPI `search_get`). PayPal rejects ranges over 31 days; use searchTransactions for longer ranges. */ export async function listTransactions(input: ListTransactionsInput) { if (!input.startDate || !input.endDate) { throw new Error('listTransactions requires startDate and endDate.') } const raw = await paypalListTransactions({ ...input, query: { start_date: toPayPalDate(input.startDate), end_date: toPayPalDate(input.endDate), fields: input.fields ?? 'all', page: input.page ?? 1, page_size: input.pageSize ?? 100, transaction_id: input.transactionId, transaction_type: input.transactionType, transaction_status: input.transactionStatus, }, }) const details = Array.isArray((raw as any)?.transaction_details) ? (raw as any).transaction_details : [] return { total_items: (raw as any)?.total_items ?? null, total_pages: (raw as any)?.total_pages ?? null, page: (raw as any)?.page ?? input.page ?? 1, items: details.map((detail: any) => input.includeRaw ? { summary: normalizeTransactionSummary(detail), raw: detail } : normalizeTransactionSummary(detail), ), } } /** * Search PayPal transactions across a date range and filter by text, email, amount, currency, invoice id, or direction. */ export async function searchTransactions(input: SearchTransactionsInput = {}) { const defaults = defaultDateRange() const startDate = input.startDate ?? defaults.start const endDate = input.endDate ?? defaults.end const chunks = splitDateRange(startDate, endDate) const matches: Array = [] let scanned = 0 for (const chunk of chunks) { const maxPages = input.maxPagesPerChunk ?? 10 for (let page = 1; page <= maxPages; page += 1) { const listed = await listTransactions({ ...input, startDate: chunk.start, endDate: chunk.end, page, pageSize: input.pageSize ?? 100, }) scanned += listed.items.length for (const item of listed.items as Array) { const summary = input.includeRaw ? item.summary : item if (transactionMatches(summary, input)) matches.push(item) } if (!listed.total_pages || page >= Number(listed.total_pages)) break } } return { criteria: { startDate: toPayPalDate(startDate), endDate: toPayPalDate(endDate), query: input.query ?? null, email: input.email ?? null, amount: input.amount ?? null, currencyCode: input.currencyCode ?? null, invoiceId: input.invoiceId ?? null, direction: input.direction ?? null, }, chunks: chunks.map((chunk) => ({ startDate: toPayPalDate(chunk.start), endDate: toPayPalDate(chunk.end), })), scanned, matches, } } /** * Return the first transaction matching searchTransactions criteria. */ export async function findTransaction(input: SearchTransactionsInput = {}) { const result = await searchTransactions(input) return { ...result, match: result.matches[0] ?? null, } } export default async function transactions(input: SearchTransactionsInput = {}) { return await searchTransactions(input) }