Skip to main content
Glama
transactions.js16.8 kB
/** * YNAB Transactions API operations * Handles transaction listing, creation, and updates */ const { API } = require('ynab'); const { logger } = require('../utils/logger'); const { tokenManager } = require('../auth/tokenManager'); const { ValidationError, NotFoundError } = require('../utils/errorHandler'); const { rateLimiter } = require('../utils/rateLimit'); /** * List transactions with optional filtering * @param {object} params - Parameters with email, budgetId, and optional filters * @returns {Promise<object>} List of transactions */ async function listTransactions(params) { if (!params.email) { throw new ValidationError('Email parameter is required'); } if (!params.budgetId) { throw new ValidationError('Budget ID parameter is required'); } // Get fresh access token const accessToken = await tokenManager.getFreshAccessToken(params.email); // Create YNAB API instance const ynabAPI = new API(accessToken); // Use rate limiter to handle YNAB API limits return await rateLimiter.executeWithRateLimit(params.email, async () => { logger.info(`Listing transactions for budget ${params.budgetId} for ${params.email}`); try { // Get parameters const sinceDate = params.since_date || params.sinceDate; const type = params.type; const limit = params.limit || undefined; // Log for debugging if (sinceDate) { logger.info(`Using date filter: ${sinceDate}`); } if (type) { logger.info(`Using type filter: ${type}`); } if (limit) { logger.info(`Using limit: ${limit}`); } // Filter by account if provided if (params.accountId) { // If account ID is provided, use getTransactionsByAccount const response = await ynabAPI.transactions.getTransactionsByAccount( params.budgetId, params.accountId, sinceDate, params.type ); // Apply limit if needed if (limit && response.data.transactions.length > limit) { response.data.transactions = response.data.transactions.slice(0, limit); } return formatTransactionsResponse(response.data); } // Filter by category if provided else if (params.categoryId) { // If category ID is provided, use getTransactionsByCategory const response = await ynabAPI.transactions.getTransactionsByCategory( params.budgetId, params.categoryId, sinceDate, params.type ); // Apply limit if needed if (limit && response.data.transactions.length > limit) { response.data.transactions = response.data.transactions.slice(0, limit); } return formatTransactionsResponse(response.data); } // Filter by payee if provided else if (params.payeeId) { // If payee ID is provided, use getTransactionsByPayee const response = await ynabAPI.transactions.getTransactionsByPayee( params.budgetId, params.payeeId, sinceDate, params.type ); // Apply limit if needed if (limit && response.data.transactions.length > limit) { response.data.transactions = response.data.transactions.slice(0, limit); } return formatTransactionsResponse(response.data); } // No specific filter provided, get all transactions else { const response = await ynabAPI.transactions.getTransactions( params.budgetId, sinceDate, params.type ); // Apply limit if needed if (limit && response.data.transactions.length > limit) { response.data.transactions = response.data.transactions.slice(0, limit); } return formatTransactionsResponse(response.data); } } catch (error) { logger.error(`Error listing transactions: ${error.message}`, error); // Check for specific error types if (error.error) { if (error.error.id === '404') { throw new NotFoundError(`Resource not found for the specified filter`); } if (error.error.id === '400') { // Detailed information for bad request errors const detailMessage = error.error.detail || error.message; throw new ValidationError(`Invalid request: ${detailMessage}`); } } // If this is already one of our custom errors, re-throw it if (error instanceof ValidationError || error instanceof NotFoundError) { throw error; } // Otherwise, wrap in a more informative error message throw new Error(`Failed to retrieve transactions: ${error.message}`); } }); } /** * Get details of a specific transaction * @param {object} params - Parameters with email, budgetId, and transactionId * @returns {Promise<object>} Transaction details */ async function getTransaction(params) { if (!params.email) { throw new ValidationError('Email parameter is required'); } if (!params.budgetId) { throw new ValidationError('Budget ID parameter is required'); } if (!params.transactionId) { throw new ValidationError('Transaction ID parameter is required'); } // Get fresh access token const accessToken = await tokenManager.getFreshAccessToken(params.email); // Create YNAB API instance const ynabAPI = new API(accessToken); // Use rate limiter to handle YNAB API limits return await rateLimiter.executeWithRateLimit(params.email, async () => { logger.info(`Getting transaction ${params.transactionId} for budget ${params.budgetId}`); try { const response = await ynabAPI.transactions.getTransactionById( params.budgetId, params.transactionId ); const transaction = response.data.transaction; // Format the transaction for display return formatTransaction(transaction); } catch (error) { // Check for 404 error if (error.error && error.error.id === '404') { throw new NotFoundError( `Transaction with ID ${params.transactionId} not found in budget ${params.budgetId}` ); } // Re-throw other errors throw error; } }); } /** * Create a new transaction * @param {object} params - Parameters with email, budgetId, and transaction data * @returns {Promise<object>} Created transaction */ async function createTransaction(params) { if (!params.email) { throw new ValidationError('Email parameter is required'); } if (!params.budgetId) { throw new ValidationError('Budget ID parameter is required'); } if (!params.transaction) { throw new ValidationError('Transaction data is required'); } // Validate required transaction fields const txn = params.transaction; if (!txn.account_id) { throw new ValidationError('Transaction must include account_id'); } if (!txn.date) { throw new ValidationError('Transaction must include date (format: YYYY-MM-DD)'); } if (txn.amount === undefined) { throw new ValidationError('Transaction must include amount'); } // Prepare transaction for API - convert amount to milliunits if needed const transactionData = { ...txn, amount: typeof txn.amount === 'number' && Math.abs(txn.amount) >= 1000 ? txn.amount // Assume it's already in milliunits if >= 1000 : Math.round(txn.amount * 1000) // Convert to milliunits }; // Get fresh access token const accessToken = await tokenManager.getFreshAccessToken(params.email); // Create YNAB API instance const ynabAPI = new API(accessToken); // Use rate limiter to handle YNAB API limits return await rateLimiter.executeWithRateLimit(params.email, async () => { logger.info(`Creating transaction for budget ${params.budgetId}`); try { // Create transaction request const request = { transaction: transactionData }; const response = await ynabAPI.transactions.createTransaction( params.budgetId, request ); // Get the created transaction const transaction = response.data.transaction; // Format the transaction for display return formatTransaction(transaction); } catch (error) { logger.error(`Error creating transaction: ${error.message}`, error); // Check for 400 error with specific details if (error.error && error.error.id === '400') { throw new ValidationError( `Invalid transaction data: ${error.error.detail || error.message}` ); } // Re-throw other errors throw error; } }); } /** * Update an existing transaction * @param {object} params - Parameters with email, budgetId, transactionId, and transaction data * @returns {Promise<object>} Updated transaction */ async function updateTransaction(params) { if (!params.email) { throw new ValidationError('Email parameter is required'); } if (!params.budgetId) { throw new ValidationError('Budget ID parameter is required'); } if (!params.transactionId) { throw new ValidationError('Transaction ID parameter is required'); } if (!params.transaction) { throw new ValidationError('Transaction data is required'); } // Prepare transaction for API const transactionData = { id: params.transactionId, ...params.transaction }; // Convert amount to milliunits if provided and needed if (transactionData.amount !== undefined) { transactionData.amount = typeof transactionData.amount === 'number' && Math.abs(transactionData.amount) >= 1000 ? transactionData.amount // Assume it's already in milliunits if >= 1000 : Math.round(transactionData.amount * 1000); // Convert to milliunits } // Get fresh access token const accessToken = await tokenManager.getFreshAccessToken(params.email); // Create YNAB API instance const ynabAPI = new API(accessToken); // Use rate limiter to handle YNAB API limits return await rateLimiter.executeWithRateLimit(params.email, async () => { logger.info(`Updating transaction ${params.transactionId} for budget ${params.budgetId}`); try { // Update transaction request const request = { transaction: transactionData }; const response = await ynabAPI.transactions.updateTransaction( params.budgetId, params.transactionId, request ); // Get the updated transaction const transaction = response.data.transaction; // Format the transaction for display return formatTransaction(transaction); } catch (error) { logger.error(`Error updating transaction: ${error.message}`, error); // Check for specific errors if (error.error) { if (error.error.id === '404') { throw new NotFoundError( `Transaction with ID ${params.transactionId} not found in budget ${params.budgetId}` ); } if (error.error.id === '400') { throw new ValidationError( `Invalid transaction data: ${error.error.detail || error.message}` ); } } // Re-throw other errors throw error; } }); } /** * Create multiple transactions at once * @param {object} params - Parameters with email, budgetId, and array of transactions * @returns {Promise<object>} Bulk transaction creation result */ async function bulkCreateTransactions(params) { if (!params.email) { throw new ValidationError('Email parameter is required'); } if (!params.budgetId) { throw new ValidationError('Budget ID parameter is required'); } if (!params.transactions || !Array.isArray(params.transactions)) { throw new ValidationError('Transactions array is required'); } if (params.transactions.length === 0) { throw new ValidationError('Transactions array cannot be empty'); } // Prepare transactions for API const transactions = params.transactions.map(txn => { // Validate required fields if (!txn.account_id) { throw new ValidationError('Each transaction must include account_id'); } if (!txn.date) { throw new ValidationError('Each transaction must include date (format: YYYY-MM-DD)'); } if (txn.amount === undefined) { throw new ValidationError('Each transaction must include amount'); } // Convert amount to milliunits if needed return { ...txn, amount: typeof txn.amount === 'number' && Math.abs(txn.amount) >= 1000 ? txn.amount // Assume it's already in milliunits if >= 1000 : Math.round(txn.amount * 1000) // Convert to milliunits }; }); // Get fresh access token const accessToken = await tokenManager.getFreshAccessToken(params.email); // Create YNAB API instance const ynabAPI = new API(accessToken); // Use rate limiter to handle YNAB API limits return await rateLimiter.executeWithRateLimit(params.email, async () => { logger.info(`Bulk creating ${transactions.length} transactions for budget ${params.budgetId}`); try { // Create bulk transaction request const request = { transactions }; const response = await ynabAPI.transactions.createTransactions( params.budgetId, request ); // Get the transaction ids and duplicates const result = response.data; return { transaction_ids: result.transaction_ids, duplicate_import_ids: result.duplicate_import_ids, transactions: result.transactions ? result.transactions.map(formatTransaction) : [] }; } catch (error) { logger.error(`Error creating bulk transactions: ${error.message}`, error); // Check for 400 error with specific details if (error.error && error.error.id === '400') { throw new ValidationError( `Invalid transaction data: ${error.error.detail || error.message}` ); } // Re-throw other errors throw error; } }); } /** * Format a single transaction for display * @param {object} transaction - Transaction object from YNAB API * @returns {object} Formatted transaction */ function formatTransaction(transaction) { // Format currency values const amountFormatted = formatCurrency(transaction.amount); return { id: transaction.id, date: transaction.date, amount: transaction.amount, amount_formatted: amountFormatted, memo: transaction.memo, cleared: transaction.cleared, approved: transaction.approved, flag_color: transaction.flag_color, account_id: transaction.account_id, account_name: transaction.account_name, payee_id: transaction.payee_id, payee_name: transaction.payee_name, category_id: transaction.category_id, category_name: transaction.category_name, transfer_account_id: transaction.transfer_account_id, transfer_transaction_id: transaction.transfer_transaction_id, matched_transaction_id: transaction.matched_transaction_id, import_id: transaction.import_id, deleted: transaction.deleted, subtransactions: transaction.subtransactions ? transaction.subtransactions.map(sub => ({ id: sub.id, amount: sub.amount, amount_formatted: formatCurrency(sub.amount), memo: sub.memo, payee_id: sub.payee_id, payee_name: sub.payee_name, category_id: sub.category_id, category_name: sub.category_name, transfer_account_id: sub.transfer_account_id, transfer_transaction_id: sub.transfer_transaction_id, deleted: sub.deleted })) : [] }; } /** * Format transactions response data * @param {object} data - Response data from YNAB API * @returns {object} Formatted response with transactions */ function formatTransactionsResponse(data) { return { transactions: data.transactions.map(formatTransaction), server_knowledge: data.server_knowledge }; } /** * Format currency amount (in milliunits) to a readable string * @param {number} amountInMilliunits - Amount in milliunits * @returns {string} Formatted currency string */ function formatCurrency(amountInMilliunits) { const amount = amountInMilliunits / 1000; return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 }).format(amount); } module.exports = { listTransactions, getTransaction, createTransaction, updateTransaction, bulkCreateTransactions };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mattweg/ynab-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server