207 lines
5.7 KiB
TypeScript
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);
|
|
}
|
|
}
|