'ops@liderra.local']); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->create(); $tier1 = mock(SupplierProjectChannel::class); $tier1->shouldReceive('listProjects')->andReturn([]); // dedup-сверка: нет совпадений $tier1->shouldReceive('createProject')->andThrow(new SupplierClientException('Tier-1 mock fail')); $tier2 = mock(SupplierProjectChannel::class); $tier2->shouldReceive('createProject')->andThrow(new RuntimeException('Tier-2 manage-project.js selector break')); $channel = new FailoverProjectChannel($tier1, $tier2, app(Mailer::class)); $dto = new SupplierProjectDto( platform: 'B1', signalType: 'site', uniqueKey: 'failover-smoke.example', limit: 1, workdays: [1, 2, 3, 4, 5], regions: [], regionsReverse: false, status: 'active', ); expect(fn () => $channel->createProjectForLiderra($project, $dto)) ->toThrow(TierEscalatedException::class); expect(SupplierManualSyncQueue::where('project_id', $project->id)->count())->toBe(1); Mail::assertQueued(SupplierCriticalAlertMail::class, fn ($m) => $m->alertType === 'manual_required'); }); test('Tier-1 transient fail (portal unreachable) bypasses Tier-2 and goes straight to Tier-3', function (): void { Mail::fake(); config(['services.supplier.alert_email' => 'ops@liderra.local']); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->create(); $tier1 = mock(SupplierProjectChannel::class); $tier1->shouldReceive('listProjects')->andReturn([]); $tier1->shouldReceive('createProject')->andThrow(new SupplierTransientException('Connection refused')); $tier2 = mock(SupplierProjectChannel::class); $tier2->shouldNotReceive('createProject'); // КЛЮЧЕВОЕ — transient НЕ должен попасть в tier-2 $channel = new FailoverProjectChannel($tier1, $tier2, app(Mailer::class)); $dto = new SupplierProjectDto( platform: 'B1', signalType: 'site', uniqueKey: 'transient-smoke.example', limit: 1, workdays: [1, 2, 3, 4, 5], regions: [], regionsReverse: false, status: 'active', ); expect(fn () => $channel->createProjectForLiderra($project, $dto)) ->toThrow(TierEscalatedException::class); $row = SupplierManualSyncQueue::where('project_id', $project->id)->first(); expect($row->failure_reason)->toBe('portal_unreachable'); });