Improve automation rules and sync error handling

- Add null safety checks in rule evaluation
- Improve error messages in sync context with user-friendly formatting
- Add browser test for automation rule application during import
- Update test CSV data to better test rule matching
- Add detailed error logging for failed transaction imports
This commit is contained in:
Víctor Falcón 2025-12-01 10:30:22 +01:00
parent a4ed4e73c1
commit 0b97570ffd
6 changed files with 121 additions and 6 deletions

View File

@ -30,6 +30,28 @@ interface SyncContextType {
const SyncContext = createContext<SyncContextType | undefined>(undefined);
function formatErrorMessage(error: string): string {
if (error.includes('status code 5')) {
return 'Server is temporarily unavailable. Please try again later.';
}
if (error.includes('status code 401') || error.includes('status code 403')) {
return 'Your session has expired. Please refresh the page.';
}
if (error.includes('status code 4')) {
return 'Something went wrong. Please try again.';
}
if (error.includes('Network Error') || error.includes('network')) {
return 'Unable to connect. Check your internet connection.';
}
if (error.includes('timeout') || error.includes('Timeout')) {
return 'The request took too long. Please try again.';
}
if (error === 'Sync already in progress') {
return 'Sync is already running. Please wait.';
}
return 'Sync failed. Please try again.';
}
const SYNC_INTERVAL = 5 * 60 * 1000;
interface SyncProviderProps {
@ -81,7 +103,7 @@ export function SyncProvider({
}
if (!isOnline) {
setError('Cannot sync while offline');
setError('Unable to sync while offline. Connect to the internet and try again.');
return;
}
@ -117,7 +139,10 @@ export function SyncProvider({
];
if (allErrors.length > 0) {
setError(allErrors.join(', '));
const uniqueFormattedErrors = [
...new Set(allErrors.map(formatErrorMessage)),
];
setError(uniqueFormattedErrors.join(' '));
setSyncStatus('error');
} else {
setSyncStatus('success');
@ -129,7 +154,9 @@ export function SyncProvider({
}
} catch (err) {
console.error('Sync failed:', err);
setError(err instanceof Error ? err.message : 'Unknown sync error');
const errorMessage =
err instanceof Error ? err.message : 'Unknown sync error';
setError(formatErrorMessage(errorMessage));
setSyncStatus('error');
setTimeout(() => {

View File

@ -3,11 +3,12 @@ import type { Account, Bank } from '@/types/account';
import type { AutomationRule } from '@/types/automation-rule';
import type { Category } from '@/types/category';
import type { DecryptedTransaction } from '@/types/transaction';
import type { UUID } from '@/types/uuid';
import jsonLogic from 'json-logic-js';
export interface RuleEvaluationResult {
rule: AutomationRule;
categoryId: number | null;
categoryId: UUID | null;
note: string | null;
noteIv: string | null;
}
@ -175,7 +176,7 @@ export interface NewTransactionData {
description: string;
amount: number;
transaction_date: string;
account_id: string;
account_id: UUID;
notes?: string;
}
@ -186,6 +187,11 @@ export function evaluateRulesForNewTransaction(
accounts: Account[],
banks: Bank[],
): RuleEvaluationResult | null {
if (!rules || !categories || !accounts || !banks) {
consoleDebug('[Rule Engine] Missing required data for rule evaluation');
return null;
}
const sortedRules = [...rules].sort((a, b) => a.priority - b.priority);
const account = accounts.find((a) => a.id === transactionData.account_id);

View File

@ -231,3 +231,84 @@ it('can navigate back through import steps', function () {
->assertSee('Select the account to import transactions into')
->assertNoJavascriptErrors();
});
it('applies automation rules when importing transactions', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
$groceriesCategory = Category::factory()->create([
'user_id' => $user->id,
'name' => 'Groceries',
]);
$bank = Bank::factory()->create(['name' => 'My Bank']);
$account = Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'My Checking',
'name_iv' => str_repeat('b', 16),
'currency_code' => 'USD',
]);
\App\Models\AutomationRule::factory()->create([
'user_id' => $user->id,
'title' => 'Auto Categorize Groceries',
'priority' => 1,
'rules_json' => [
'in' => [
'walmart',
['var' => 'description'],
],
],
'action_category_id' => $groceriesCategory->id,
'action_note' => null,
'action_note_iv' => null,
]);
actingAs($user);
$page = visit('/transactions');
$testFile = __DIR__.'/assets/test-transactions.csv';
$page->assertSee('Transactions')
->click('button[aria-label="More actions"]')
->wait(1)
->press('ArrowDown')
->wait(0.2)
->press('Enter')
->wait(1)
->click('label')
->wait(0.5)
->click('Next')
->waitFor('Drop your file here')
->attach('input[type="file"]', $testFile)
->wait(1)
->click('Next')
->wait(1)
->click('#date-column')
->wait(0.3)
->click('Date')
->click('#description-column')
->wait(0.3)
->click('Description')
->click('#amount-column')
->wait(0.3)
->click('Amount')
->wait(0.5)
->click('Preview Transactions')
->wait(2)
->click('Import')
->wait(3)
->assertSee('imported successfully');
$page->wait(2);
expect(\App\Models\Transaction::where('user_id', $user->id)->count())->toBeGreaterThan(0);
$walmartTransaction = \App\Models\Transaction::where('user_id', $user->id)
->whereRaw('LOWER(description) LIKE ?', ['%walmart%'])
->first();
expect($walmartTransaction)->not->toBeNull();
expect($walmartTransaction->category_id)->toBe($groceriesCategory->id);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,7 +1,8 @@
Date,Description,Amount
2024-01-15,Grocery Store Purchase,-50.25
2024-01-15,Walmart Supercenter,-50.25
2024-01-16,Coffee Shop,-5.50
2024-01-17,Salary Deposit,2500.00
2024-01-18,Electric Bill,-125.00
2024-01-19,Restaurant Dinner,-45.75

1 Date Description Amount
2 2024-01-15 Grocery Store Purchase Walmart Supercenter -50.25
3 2024-01-16 Coffee Shop -5.50
4 2024-01-17 Salary Deposit 2500.00
5 2024-01-18 Electric Bill -125.00
6 2024-01-19 Restaurant Dinner -45.75
7
8