diff --git a/app/Console/Commands/SyncBankingConnections.php b/app/Console/Commands/SyncBankingConnections.php index bec292ad..642d0523 100644 --- a/app/Console/Commands/SyncBankingConnections.php +++ b/app/Console/Commands/SyncBankingConnections.php @@ -2,20 +2,70 @@ namespace App\Console\Commands; +use App\Enums\BankingConnectionStatus; use App\Jobs\SyncAllBankingConnectionsJob; +use App\Jobs\SyncBankingConnectionJob; +use App\Models\BankingConnection; +use App\Models\User; use Illuminate\Console\Command; class SyncBankingConnections extends Command { - protected $signature = 'banking:sync'; + protected $signature = 'banking:sync + {--user= : Filter by user email address} + {--connection= : Filter by banking connection ID}'; protected $description = 'Sync transactions and balances for all active banking connections'; public function handle(): int { - SyncAllBankingConnectionsJob::dispatch(); + $userEmail = $this->option('user'); + $connectionId = $this->option('connection'); - $this->info('Banking sync jobs dispatched.'); + if (! $userEmail && ! $connectionId) { + SyncAllBankingConnectionsJob::dispatch(); + + $this->info('Banking sync jobs dispatched for all active connections.'); + + return Command::SUCCESS; + } + + $query = BankingConnection::query() + ->where('status', BankingConnectionStatus::Active) + ->where(function ($query) { + $query->whereNull('valid_until') + ->orWhere('valid_until', '>', now()); + }); + + if ($connectionId) { + $query->where('id', $connectionId); + } + + if ($userEmail) { + $user = User::query()->where('email', $userEmail)->first(); + + if (! $user) { + $this->error("User with email '{$userEmail}' not found."); + + return Command::FAILURE; + } + + $query->where('user_id', $user->id); + } + + $connections = $query->get(); + + if ($connections->isEmpty()) { + $this->warn('No active banking connections found matching the given filters.'); + + return Command::SUCCESS; + } + + $connections->each(function (BankingConnection $connection) { + SyncBankingConnectionJob::dispatch($connection); + }); + + $this->info("Banking sync jobs dispatched for {$connections->count()} connection(s)."); return Command::SUCCESS; } diff --git a/tests/Feature/SyncBankingConnectionsCommandTest.php b/tests/Feature/SyncBankingConnectionsCommandTest.php new file mode 100644 index 00000000..0b1a7261 --- /dev/null +++ b/tests/Feature/SyncBankingConnectionsCommandTest.php @@ -0,0 +1,105 @@ +expectsOutputToContain('Banking sync jobs dispatched for all active connections.') + ->assertSuccessful(); + + Queue::assertPushed(SyncAllBankingConnectionsJob::class); +}); + +test('banking:sync filters by user email', function () { + Queue::fake(); + + $user = User::factory()->create(['email' => 'test@example.com']); + $connection = BankingConnection::factory()->for($user)->create(); + + // Another user's connection that should NOT be synced + BankingConnection::factory()->create(); + + artisan('banking:sync', ['--user' => 'test@example.com']) + ->expectsOutputToContain('Banking sync jobs dispatched for 1 connection(s).') + ->assertSuccessful(); + + Queue::assertPushed(SyncBankingConnectionJob::class, 1); + Queue::assertPushed(SyncBankingConnectionJob::class, function ($job) use ($connection) { + return $job->bankingConnection->id === $connection->id; + }); + Queue::assertNotPushed(SyncAllBankingConnectionsJob::class); +}); + +test('banking:sync filters by connection ID', function () { + Queue::fake(); + + $connection = BankingConnection::factory()->create(); + BankingConnection::factory()->create(); + + artisan('banking:sync', ['--connection' => $connection->id]) + ->expectsOutputToContain('Banking sync jobs dispatched for 1 connection(s).') + ->assertSuccessful(); + + Queue::assertPushed(SyncBankingConnectionJob::class, 1); + Queue::assertPushed(SyncBankingConnectionJob::class, function ($job) use ($connection) { + return $job->bankingConnection->id === $connection->id; + }); +}); + +test('banking:sync fails when user email is not found', function () { + artisan('banking:sync', ['--user' => 'nonexistent@example.com']) + ->expectsOutputToContain("User with email 'nonexistent@example.com' not found.") + ->assertFailed(); +}); + +test('banking:sync warns when no active connections match filters', function () { + Queue::fake(); + + $user = User::factory()->create(['email' => 'test@example.com']); + BankingConnection::factory()->expired()->for($user)->create(); + + artisan('banking:sync', ['--user' => 'test@example.com']) + ->expectsOutputToContain('No active banking connections found matching the given filters.') + ->assertSuccessful(); + + Queue::assertNotPushed(SyncBankingConnectionJob::class); +}); + +test('banking:sync skips expired connections when filtering by user', function () { + Queue::fake(); + + $user = User::factory()->create(['email' => 'test@example.com']); + BankingConnection::factory()->for($user)->create(); + BankingConnection::factory()->expired()->for($user)->create(); + + artisan('banking:sync', ['--user' => 'test@example.com']) + ->expectsOutputToContain('Banking sync jobs dispatched for 1 connection(s).') + ->assertSuccessful(); + + Queue::assertPushed(SyncBankingConnectionJob::class, 1); +}); + +test('banking:sync can combine user and connection filters', function () { + Queue::fake(); + + $user = User::factory()->create(['email' => 'test@example.com']); + $connection = BankingConnection::factory()->for($user)->create(); + BankingConnection::factory()->for($user)->create(); + + artisan('banking:sync', ['--user' => 'test@example.com', '--connection' => $connection->id]) + ->expectsOutputToContain('Banking sync jobs dispatched for 1 connection(s).') + ->assertSuccessful(); + + Queue::assertPushed(SyncBankingConnectionJob::class, 1); + Queue::assertPushed(SyncBankingConnectionJob::class, function ($job) use ($connection) { + return $job->bankingConnection->id === $connection->id; + }); +});