diff --git a/app/Http/Controllers/Sync/AccountBalanceSyncController.php b/app/Http/Controllers/Sync/AccountBalanceSyncController.php index 3c97c8bb..bd90bfc7 100644 --- a/app/Http/Controllers/Sync/AccountBalanceSyncController.php +++ b/app/Http/Controllers/Sync/AccountBalanceSyncController.php @@ -35,22 +35,33 @@ class AccountBalanceSyncController extends Controller { $data = $request->validated(); - $balance = new AccountBalance([ - 'account_id' => $data['account_id'], - 'balance_date' => $data['balance_date'], - 'balance' => $data['balance'], - ]); + $existing = AccountBalance::where('account_id', $data['account_id']) + ->where('balance_date', $data['balance_date']) + ->first(); - if (isset($data['id'])) { - $balance->id = $data['id']; - $balance->exists = false; + if ($existing) { + $existing->update(['balance' => $data['balance']]); + $balance = $existing; + $wasRecentlyCreated = false; + } else { + $balance = new AccountBalance([ + 'account_id' => $data['account_id'], + 'balance_date' => $data['balance_date'], + 'balance' => $data['balance'], + ]); + + if (isset($data['id'])) { + $balance->id = $data['id']; + $balance->exists = false; + } + + $balance->save(); + $wasRecentlyCreated = true; } - $balance->save(); - return response()->json([ - 'data' => $balance, - ], 201); + 'data' => $balance->fresh(), + ], $wasRecentlyCreated ? 201 : 200); } public function update(StoreAccountBalanceRequest $request, AccountBalance $accountBalance): JsonResponse diff --git a/resources/js/components/transactions/import-transactions-drawer.tsx b/resources/js/components/transactions/import-transactions-drawer.tsx index 393385d2..9838a7b1 100644 --- a/resources/js/components/transactions/import-transactions-drawer.tsx +++ b/resources/js/components/transactions/import-transactions-drawer.tsx @@ -22,6 +22,8 @@ import { evaluateRulesForNewTransaction } from '@/lib/rule-engine'; import { accountBalanceSyncService } from '@/services/account-balance-sync'; import { accountSyncService } from '@/services/account-sync'; import { automationRuleSyncService } from '@/services/automation-rule-sync'; +import { bankSyncService } from '@/services/bank-sync'; +import { categorySyncService } from '@/services/category-sync'; import { transactionSyncService } from '@/services/transaction-sync'; import { type Account } from '@/types/account'; import { @@ -331,6 +333,10 @@ export function ImportTransactionsDrawer({ const key = keyString ? await importKey(keyString) : null; const rules = key ? await automationRuleSyncService.getAll() : []; + const freshAccounts = await accountSyncService.getAll(); + const freshBanks = await bankSyncService.getAll(); + const freshCategories = await categorySyncService.getAll(); + const BATCH_SIZE = 20; let processedCount = 0; @@ -359,9 +365,9 @@ export function ImportTransactionsDrawer({ account_id: selectedAccount.id, }, rules, - categories, - accounts, - banks, + freshCategories, + freshAccounts, + freshBanks, ); if (ruleMatch) { diff --git a/resources/js/lib/rule-engine.ts b/resources/js/lib/rule-engine.ts index e160e1c4..641cee88 100644 --- a/resources/js/lib/rule-engine.ts +++ b/resources/js/lib/rule-engine.ts @@ -188,7 +188,16 @@ export function evaluateRulesForNewTransaction( banks: Bank[], ): RuleEvaluationResult | null { if (!rules || !categories || !accounts || !banks) { - consoleDebug('[Rule Engine] Missing required data for rule evaluation'); + consoleDebug('[Rule Engine] Missing required data for rule evaluation', { + hasRules: !!rules, + rulesLength: rules?.length, + hasCategories: !!categories, + categoriesLength: categories?.length, + hasAccounts: !!accounts, + accountsLength: accounts?.length, + hasBanks: !!banks, + banksLength: banks?.length, + }); return null; } diff --git a/resources/js/services/account-balance-sync.ts b/resources/js/services/account-balance-sync.ts index f8cc2995..8669698c 100644 --- a/resources/js/services/account-balance-sync.ts +++ b/resources/js/services/account-balance-sync.ts @@ -38,45 +38,27 @@ class AccountBalanceSyncService { async create( data: Omit, ): Promise { - return await this.syncManager.create< - AccountBalance, - Omit & { - id?: number; - created_at?: string; - updated_at?: string; - } - >(data); + return await this.syncManager.createLocal(data); } async createMany( balances: Omit[], ): Promise { try { - const timestamp = new Date().toISOString(); const created: AccountBalance[] = []; for (const data of balances) { - const record = { - ...data, - id: uuidv7(), - created_at: timestamp, - updated_at: timestamp, - } as AccountBalance; - - await db.account_balances.put(record); - await db.pending_changes.add({ - store: 'account_balances', - operation: 'create', - data: record, - timestamp, - }); - - created.push(record); + const result = await this.updateOrCreate( + data.account_id, + data.balance_date, + data.balance, + ); + created.push(result); } return created; } catch (error) { - console.error('Failed to create balances in IndexedDB:', error); + console.error('Failed to create balances:', error); throw new Error( 'Failed to save balances locally. Please refresh the page and try again.', ); @@ -103,8 +85,22 @@ class AccountBalanceSyncService { .first(); if (existing) { - await this.update(existing.id, { balance }); - return (await this.getById(existing.id))!; + const timestamp = new Date().toISOString(); + const updated = { + ...existing, + balance, + updated_at: timestamp, + }; + + await db.account_balances.put(updated); + await db.pending_changes.add({ + store: 'account_balances', + operation: 'update', + data: { id: existing.id, balance }, + timestamp, + }); + + return updated; } else { return await this.create({ account_id: accountId, 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 index 739069a7..b5bc5ef1 100644 Binary files a/tests/Browser/Screenshots/it_applies_automation_rules_when_importing_transactions.png and b/tests/Browser/Screenshots/it_applies_automation_rules_when_importing_transactions.png differ diff --git a/tests/Browser/Screenshots/it_can_complete_full_import_flow.png b/tests/Browser/Screenshots/it_can_complete_full_import_flow.png new file mode 100644 index 00000000..4d06b7fd Binary files /dev/null and b/tests/Browser/Screenshots/it_can_complete_full_import_flow.png differ diff --git a/tests/Browser/Screenshots/it_can_navigate_back_through_import_steps.png b/tests/Browser/Screenshots/it_can_navigate_back_through_import_steps.png new file mode 100644 index 00000000..000d22ba Binary files /dev/null and b/tests/Browser/Screenshots/it_can_navigate_back_through_import_steps.png differ diff --git a/tests/Browser/Screenshots/it_can_open_import_transactions_drawer.png b/tests/Browser/Screenshots/it_can_open_import_transactions_drawer.png new file mode 100644 index 00000000..c688c9ec Binary files /dev/null and b/tests/Browser/Screenshots/it_can_open_import_transactions_drawer.png differ diff --git a/tests/Browser/Screenshots/it_can_select_account_for_import.png b/tests/Browser/Screenshots/it_can_select_account_for_import.png new file mode 100644 index 00000000..40d0e7a0 Binary files /dev/null and b/tests/Browser/Screenshots/it_can_select_account_for_import.png differ diff --git a/tests/Browser/Screenshots/it_can_upload_a_CSV_file_for_import.png b/tests/Browser/Screenshots/it_can_upload_a_CSV_file_for_import.png new file mode 100644 index 00000000..7da391ee Binary files /dev/null and b/tests/Browser/Screenshots/it_can_upload_a_CSV_file_for_import.png differ diff --git a/tests/Browser/Screenshots/it_shows_column_mapping_step_after_file_upload.png b/tests/Browser/Screenshots/it_shows_column_mapping_step_after_file_upload.png new file mode 100644 index 00000000..49cbe47e Binary files /dev/null and b/tests/Browser/Screenshots/it_shows_column_mapping_step_after_file_upload.png differ diff --git a/tests/Browser/Screenshots/it_shows_no_accounts_message_when_none_exist.png b/tests/Browser/Screenshots/it_shows_no_accounts_message_when_none_exist.png new file mode 100644 index 00000000..4580e7ce Binary files /dev/null and b/tests/Browser/Screenshots/it_shows_no_accounts_message_when_none_exist.png differ diff --git a/tests/Feature/AccountBalanceSyncControllerTest.php b/tests/Feature/AccountBalanceSyncControllerTest.php index e3d5b795..9552e0b8 100644 --- a/tests/Feature/AccountBalanceSyncControllerTest.php +++ b/tests/Feature/AccountBalanceSyncControllerTest.php @@ -165,12 +165,12 @@ it('cannot update another user account balance', function () { $response->assertForbidden(); }); -it('enforces unique constraint on account_id and balance_date', function () { +it('updates existing balance when creating with duplicate account_id and balance_date', function () { $user = User::factory()->create(); $account = Account::factory()->for($user)->create(); - $date = now()->toDateString(); + $date = '2025-01-15'; - AccountBalance::factory()->for($account)->create([ + $initialBalance = AccountBalance::factory()->for($account)->create([ 'balance_date' => $date, 'balance' => 100000, ]); @@ -178,10 +178,16 @@ it('enforces unique constraint on account_id and balance_date', function () { $balanceData = [ 'account_id' => $account->id, 'balance_date' => $date, - 'balance' => 200000, + 'balance' => 250000, ]; $response = $this->actingAs($user)->postJson('/api/sync/account-balances', $balanceData); - $response->assertStatus(500); + $response->assertOk() + ->assertJsonPath('data.id', $initialBalance->id) + ->assertJsonPath('data.balance', 250000); + + expect(AccountBalance::where('account_id', $account->id) + ->where('balance_date', $date) + ->count())->toBe(1); });