diff --git a/resources/js/contexts/sync-context.tsx b/resources/js/contexts/sync-context.tsx index eb24626c..02522c23 100644 --- a/resources/js/contexts/sync-context.tsx +++ b/resources/js/contexts/sync-context.tsx @@ -30,6 +30,28 @@ interface SyncContextType { const SyncContext = createContext(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(() => { diff --git a/resources/js/lib/rule-engine.ts b/resources/js/lib/rule-engine.ts index 606a6914..e160e1c4 100644 --- a/resources/js/lib/rule-engine.ts +++ b/resources/js/lib/rule-engine.ts @@ -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); diff --git a/tests/Browser/ImportTransactionsTest.php b/tests/Browser/ImportTransactionsTest.php index d6714ada..6bf229b6 100644 --- a/tests/Browser/ImportTransactionsTest.php +++ b/tests/Browser/ImportTransactionsTest.php @@ -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); +}); diff --git a/tests/Browser/Screenshots/it_applies_automation_rules_when_importing_transactions.png b/tests/Browser/Screenshots/it_applies_automation_rules_when_importing_transactions.png new file mode 100644 index 00000000..739069a7 Binary files /dev/null and b/tests/Browser/Screenshots/it_applies_automation_rules_when_importing_transactions.png differ diff --git a/tests/Browser/Screenshots/it_can_type_integer_values_in_amount_field.png b/tests/Browser/Screenshots/it_can_type_integer_values_in_amount_field.png deleted file mode 100644 index 11facfa7..00000000 Binary files a/tests/Browser/Screenshots/it_can_type_integer_values_in_amount_field.png and /dev/null differ diff --git a/tests/Browser/assets/test-transactions.csv b/tests/Browser/assets/test-transactions.csv index ac5daee7..38f290f1 100644 --- a/tests/Browser/assets/test-transactions.csv +++ b/tests/Browser/assets/test-transactions.csv @@ -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 +