feat: Add --user and --connection filters to banking:sync command (#122)

## Summary
- Added `--user={email}` option to filter sync by a specific user's
email address
- Added `--connection={id}` option to filter sync by a specific banking
connection ID
- Both filters can be combined; when neither is provided, the command
dispatches the bulk sync job as before
- Only active, non-expired connections are synced when filters are used

## Test plan
- [x] 7 new Pest tests covering all filter scenarios (all passing)
- [x] Pint formatting passes
This commit is contained in:
Víctor Falcón 2026-02-13 17:35:07 +01:00 committed by GitHub
parent 6b05de173a
commit b9abf49617
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 158 additions and 3 deletions

View File

@ -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;
}

View File

@ -0,0 +1,105 @@
<?php
use App\Jobs\SyncAllBankingConnectionsJob;
use App\Jobs\SyncBankingConnectionJob;
use App\Models\BankingConnection;
use App\Models\User;
use Illuminate\Support\Facades\Queue;
use function Pest\Laravel\artisan;
test('banking:sync dispatches job for all connections when no filters provided', function () {
Queue::fake();
artisan('banking:sync')
->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;
});
});