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:
parent
6b05de173a
commit
b9abf49617
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue