import * as ynab from 'ynab'; import type { YNABTransaction, YNABCategory } from './types'; export class YNABClient { private api: ynab.API; private budgetId: string; constructor(accessToken: string, budgetId: string) { this.api = new ynab.API(accessToken); this.budgetId = budgetId; } /** * Fetch unapproved transactions * @returns Array of unapproved YNAB transactions */ async getUnapprovedTransactions(): Promise { try { const response = await this.api.transactions.getTransactions( this.budgetId, undefined, // sinceDate - get all 'unapproved' // type filter for unapproved transactions ); const transactions = response.data.transactions .filter((t) => { // Only include outflow transactions (purchases) return t.amount < 0; }) .map((t) => this.mapTransaction(t)); return transactions; } catch (error) { console.error('Error fetching unapproved YNAB transactions:', error); throw error; } } /** * Fetch transactions for a given date range * @param startDate - Start date (YYYY-MM-DD) * @param endDate - End date (YYYY-MM-DD) * @returns Array of YNAB transactions */ async getTransactions(startDate: string, endDate?: string): Promise { try { const response = await this.api.transactions.getTransactions( this.budgetId, startDate, undefined // type filter ); const transactions = response.data.transactions .filter((t) => { // Filter by date range if endDate is provided if (endDate && t.date > endDate) { return false; } // Only include outflow transactions (purchases) return t.amount < 0; }) .map((t) => this.mapTransaction(t)); return transactions; } catch (error) { console.error('Error fetching YNAB transactions:', error); throw error; } } /** * Get all budget categories * @returns Array of YNAB categories */ async getCategories(): Promise { try { const response = await this.api.categories.getCategories(this.budgetId); const categories: YNABCategory[] = []; for (const group of response.data.category_groups) { if (group.hidden || group.deleted) continue; for (const category of group.categories) { if (category.hidden || category.deleted) continue; categories.push({ id: category.id, name: category.name, category_group_id: group.id, category_group_name: group.name, }); } } return categories; } catch (error) { console.error('Error fetching YNAB categories:', error); throw error; } } /** * Update a transaction's memo, category, and approval status * @param transactionId - Transaction ID * @param memo - New memo text * @param categoryId - Optional category ID * @param approved - Whether to mark as approved (default: true) */ async updateTransaction( transactionId: string, memo: string, categoryId?: string, approved: boolean = true ): Promise { try { const updateData: ynab.SaveTransaction = { memo, approved, }; if (categoryId) { updateData.category_id = categoryId; } await this.api.transactions.updateTransaction( this.budgetId, transactionId, { transaction: updateData } ); console.log(`Updated transaction ${transactionId}${approved ? ' and marked as approved' : ''}`); } catch (error) { console.error(`Error updating transaction ${transactionId}:`, error); throw error; } } /** * Get account name for a transaction * @param accountId - Account ID * @returns Account name */ async getAccountName(accountId: string): Promise { try { const response = await this.api.accounts.getAccounts(this.budgetId); const account = response.data.accounts.find((a) => a.id === accountId); return account?.name || 'Unknown Account'; } catch (error) { console.error('Error fetching account name:', error); return 'Unknown Account'; } } /** * Get category name for a transaction * @param categoryId - Category ID * @returns Category name */ async getCategoryName(categoryId: string | null): Promise { if (!categoryId) return null; try { const categories = await this.getCategories(); const category = categories.find((c) => c.id === categoryId); return category?.name || null; } catch (error) { console.error('Error fetching category name:', error); return null; } } /** * Map YNAB API transaction to our internal format */ private mapTransaction(transaction: ynab.TransactionDetail): YNABTransaction { return { id: transaction.id, date: transaction.date, amount: transaction.amount, payee_name: transaction.payee_name, memo: transaction.memo, category_id: transaction.category_id, category_name: transaction.category_name, account_id: transaction.account_id, account_name: transaction.account_name, }; } /** * Convert YNAB milliunits to dollars * @param milliunits - Amount in milliunits * @returns Amount in dollars */ static milliunitsToDollars(milliunits: number): number { return milliunits / 1000; } /** * Convert dollars to YNAB milliunits * @param dollars - Amount in dollars * @returns Amount in milliunits */ static dollarsToMilliunits(dollars: number): number { return Math.round(dollars * 1000); } }