ynab-amazon-helper/ynab-client.ts
2025-11-04 12:27:02 -05:00

207 lines
5.7 KiB
TypeScript

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<YNABTransaction[]> {
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<YNABTransaction[]> {
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<YNABCategory[]> {
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<void> {
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<string> {
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<string | null> {
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);
}
}