Skip to content

Commit

Permalink
Do not process off budget accounts (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
sakowicz authored Jan 26, 2025
1 parent c4b4f4f commit 1a6f9b6
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 12 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sakowicz/actual-ai",
"version": "1.7.1",
"version": "1.7.2",
"description": "Transaction AI classification for Actual Budget app.",
"main": "app.js",
"scripts": {
Expand Down
5 changes: 5 additions & 0 deletions src/actual-api-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
APIAccountEntity,
APICategoryEntity,
APICategoryGroupEntity,
APIPayeeEntity,
Expand Down Expand Up @@ -85,6 +86,10 @@ class ActualApiService implements ActualApiServiceI {
return this.actualApiClient.getPayees();
}

public async getAccounts(): Promise<APIAccountEntity[]> {
return this.actualApiClient.getAccounts();
}

public async getTransactions(): Promise<TransactionEntity[]> {
return this.actualApiClient.getTransactions(undefined, undefined, undefined);
}
Expand Down
6 changes: 5 additions & 1 deletion src/transaction-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ class TransactionService implements TransactionServiceI {
const categories = await this.actualApiService.getCategories();
const payees = await this.actualApiService.getPayees();
const transactions = await this.actualApiService.getTransactions();
const accounts = await this.actualApiService.getAccounts();
const accountsToSkip = accounts.filter((account) => account.offbudget)
.map((account) => account.id);

const uncategorizedTransactions = transactions.filter(
(transaction) => !transaction.category
Expand All @@ -84,7 +87,8 @@ class TransactionService implements TransactionServiceI {
&& transaction.imported_payee !== null
&& transaction.imported_payee !== ''
&& (transaction.notes === null || (!transaction.notes?.includes(this.notGuessedTag)))
&& !transaction.is_parent,
&& !transaction.is_parent
&& !accountsToSkip.includes(transaction.account),
);

for (let i = 0; i < uncategorizedTransactions.length; i++) {
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LanguageModel } from 'ai';
import {
APIAccountEntity,
APICategoryEntity,
APICategoryGroupEntity,
APIPayeeEntity,
Expand All @@ -19,6 +20,8 @@ export interface ActualApiServiceI {

getCategories(): Promise<(APICategoryEntity | APICategoryGroupEntity)[]>

getAccounts(): Promise<APIAccountEntity[]>

getPayees(): Promise<APIPayeeEntity[]>

getTransactions(): Promise<TransactionEntity[]>
Expand Down
31 changes: 30 additions & 1 deletion tests/actual-ai.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
APIAccountEntity,
APICategoryEntity,
APICategoryGroupEntity,
APIPayeeEntity,
Expand All @@ -16,7 +17,7 @@ describe('ActualAiService', () => {
let inMemoryApiService: InMemoryActualApiService;
let mockedLlmService: MockedLlmService;
let mockedPromptGenerator: MockedPromptGenerator;
let syncAccountsBeforeClassify = true;
let syncAccountsBeforeClassify = false;
const GUESSED_TAG = '#actual-ai';
const NOT_GUESSED_TAG = '#actual-ai-miss';

Expand All @@ -27,6 +28,7 @@ describe('ActualAiService', () => {
const categoryGroups: APICategoryGroupEntity[] = GivenActualData.createSampleCategoryGroups();
const categories: APICategoryEntity[] = GivenActualData.createSampleCategories();
const payees: APIPayeeEntity[] = GivenActualData.createSamplePayees();
const accounts: APIAccountEntity[] = GivenActualData.createSampleAccounts();
transactionService = new TransactionService(
inMemoryApiService,
mockedLlmService,
Expand All @@ -37,6 +39,7 @@ describe('ActualAiService', () => {
inMemoryApiService.setCategoryGroups(categoryGroups);
inMemoryApiService.setCategories(categories);
inMemoryApiService.setPayees(payees);
inMemoryApiService.setAccounts(accounts);
});

it('It should assign a category to transaction', async () => {
Expand All @@ -63,6 +66,32 @@ describe('ActualAiService', () => {
expect(updatedTransactions[0].category).toBe(GivenActualData.CATEGORY_GROCERIES);
});

it('It should process off-budget transaction when flag is set to false', async () => {
// Arrange
const transactionOffBudget = GivenActualData.createTransaction(
'1',
-123,
'Carrefour 1234',
'Carrefour XXXX1234567 822-307-2000',
undefined,
GivenActualData.ACCOUNT_OFF_BUDGET,
);
inMemoryApiService.setTransactions([transactionOffBudget]);
mockedLlmService.setGuess(GivenActualData.CATEGORY_GROCERIES);

// Act
sut = new ActualAiService(
transactionService,
inMemoryApiService,
syncAccountsBeforeClassify,
);
await sut.classify();

// Assert
const updatedTransactions = await inMemoryApiService.getTransactions();
expect(updatedTransactions[0].category).toBe(undefined);
});

it('It should assign a notes to guessed transaction', async () => {
// Arrange
const transaction = GivenActualData.createTransaction(
Expand Down
27 changes: 20 additions & 7 deletions tests/test-doubles/given/given-actual-data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
APIAccountEntity,
APICategoryEntity,
APICategoryGroupEntity,
APIPayeeEntity,
Expand All @@ -12,6 +13,10 @@ export default class GivenActualData {

public static CATEGORY_SALARY = '123836f1-e756-4473-a5d0-6c1d3f06c7fa';

public static ACCOUNT_OFF_BUDGET = '321836f1-e756-4473-a5d0-6c1d3f06c7fa';

public static ACCOUNT_MAIN = '333836f1-e756-4473-a5d0-6c1d3f06c7fa';

public static PAYEE_AIRBNB = '1';

public static PAYEE_CARREFOUR = '2';
Expand All @@ -34,13 +39,24 @@ export default class GivenActualData {
return { id, name };
}

public static createAccount(
id: string,
name: string,
isOffBudget: boolean,
isClosed: boolean,
): APIAccountEntity {
return {
id, name, offbudget: isOffBudget, closed: isClosed,
};
}

public static createTransaction(
id: string,
amount: number,
importedPayee: string,
notes = '',
payee: undefined | string = undefined,
account = '1',
account = GivenActualData.ACCOUNT_MAIN,
date = '2021-01-01',
isParent = false,
category: undefined | string = undefined,
Expand Down Expand Up @@ -87,13 +103,10 @@ export default class GivenActualData {
];
}

public static createSampleTransactions(): TransactionEntity[] {
public static createSampleAccounts(): APIAccountEntity[] {
return [
this.createTransaction('1', 100, 'Carrefour 32321', 'Transaction without category'),
this.createTransaction('2', 100, 'Carrefour 32321', 'Transaction with Groceries category', GivenActualData.CATEGORY_GROCERIES),
this.createTransaction('3', 100, 'Airbnb * XXXX1234567', 'Transaction with Travel category', undefined, GivenActualData.PAYEE_AIRBNB),
this.createTransaction('4', -30000, ' 3', 'Transaction with salary income', GivenActualData.CATEGORY_SALARY, GivenActualData.PAYEE_GOOGLE),
this.createTransaction('5', -30000, '1', 'Transaction with income without category', undefined, GivenActualData.PAYEE_GOOGLE),
this.createAccount(GivenActualData.ACCOUNT_MAIN, 'Main Account', false, false),
this.createAccount(GivenActualData.ACCOUNT_OFF_BUDGET, 'Off Budget Account', true, false),
];
}
}
11 changes: 11 additions & 0 deletions tests/test-doubles/in-memory-actual-api-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
APIAccountEntity,
APICategoryEntity,
APICategoryGroupEntity,
APIPayeeEntity,
Expand All @@ -13,6 +14,8 @@ export default class InMemoryActualApiService implements ActualApiServiceI {

private payees: APIPayeeEntity[] = [];

private accounts: APIAccountEntity[] = [];

private transactions: TransactionEntity[] = [];

private wasBankSyncRan = false;
Expand Down Expand Up @@ -41,6 +44,14 @@ export default class InMemoryActualApiService implements ActualApiServiceI {
this.categories = categories;
}

async getAccounts(): Promise<APIAccountEntity[]> {
return Promise.resolve(this.accounts);
}

setAccounts(accounts: APIAccountEntity[]): void {
this.accounts = accounts;
}

async getPayees(): Promise<APIPayeeEntity[]> {
return Promise.resolve(this.payees);
}
Expand Down

0 comments on commit 1a6f9b6

Please sign in to comment.