in('Feature')). // DatabaseTransactions — per-test isolation. // SharesSupplierPdo — SyncSupplierProjectJob пишет через pgsql_supplier (BYPASSRLS); // без шаринга PDO записи джоба не видны default-connection ассертам под DatabaseTransactions. uses(DatabaseTransactions::class, SharesSupplierPdo::class); /** * Хелпер: разрешает SupplierProjectChannel из контейнера и вызывает Job.handle(). */ function dispatchJobSync(SyncSupplierProjectJob $job): void { $job->handle(app(SupplierProjectChannel::class)); } /** * Прод-инцидент 14.07.2026 (см. SyncSupplierProjectJobTest в tests/Feature/Supplier): * batch-режим больше НЕ ходит к поставщику при создании — кабинет всё равно отбивает * каркас с limit=0 («Введите limit!»), а строки создаёт вечерний прогон 18:00 МСК. * * Раньше этот файл проверял разбор проекта на площадки ЧЕРЕЗ вызовы createProject. * Сам разбор (site/call → B1+B2+B3, sms+keyword → B2+B3, sms без keyword → B3) никуда * не делся — проверяем его напрямую у SupplierProjectGrouping, без похода к поставщику. */ it('site project resolves to B1+B2+B3', function () { $project = Project::factory()->create([ 'signal_type' => 'site', 'signal_identifier' => 'okna.ru', ]); expect(SupplierProjectGrouping::resolvePlatforms($project))->toBe(['B1', 'B2', 'B3']); }); it('call project resolves to B1+B2+B3', function () { $project = Project::factory()->create([ 'signal_type' => 'call', 'signal_identifier' => '79161234567', ]); expect(SupplierProjectGrouping::resolvePlatforms($project))->toBe(['B1', 'B2', 'B3']); }); it('sms project with keyword resolves to B2+B3 only (no B1)', function () { $project = Project::factory()->create([ 'signal_type' => 'sms', 'sms_senders' => ['TINKOFF'], 'sms_keyword' => 'ипотека', ]); expect(SupplierProjectGrouping::resolvePlatforms($project))->toBe(['B2', 'B3']); }); it('sms project without keyword resolves to B3 only', function () { $project = Project::factory()->create([ 'signal_type' => 'sms', 'sms_senders' => ['TINKOFF'], 'sms_keyword' => null, ]); expect(SupplierProjectGrouping::resolvePlatforms($project))->toBe(['B3']); }); it('batch create: supplier is never called and nothing lands in the manual queue', function () { $tenant = Tenant::factory()->create(); $project = Project::factory()->create([ 'tenant_id' => $tenant->id, 'signal_type' => 'site', 'signal_identifier' => 'okna.ru', ]); $this->mock(SupplierProjectChannel::class, function ($mock) { $mock->shouldNotReceive('createProject'); }); dispatchJobSync(new SyncSupplierProjectJob($project->id)); $project->refresh(); expect($project->supplier_b1_project_id)->toBeNull(); expect(SupplierProject::where('unique_key', 'okna.ru')->count())->toBe(0); expect(DB::table('supplier_manual_sync_queue')->where('project_id', $project->id)->count())->toBe(0); }); it('idempotency: pre-existing supplier_project row is linked, channel still not called', function () { $project = Project::factory()->create([ 'signal_type' => 'site', 'signal_identifier' => 'x.ru', ]); // B2 уже существует локально (например, создан вечерним прогоном). $spB2 = SupplierProject::factory()->create([ 'platform' => 'B2', 'signal_type' => 'site', 'unique_key' => 'x.ru', 'sync_status' => 'failed', ]); $this->mock(SupplierProjectChannel::class, function ($mock) { $mock->shouldNotReceive('createProject'); }); dispatchJobSync(new SyncSupplierProjectJob($project->id)); $project->refresh(); // Существующая строка привязана... expect($project->supplier_b2_project_id)->toBe($spB2->id); // ...а недостающие B1/B3 остаются на вечерний прогон. expect($project->supplier_b1_project_id)->toBeNull(); expect($project->supplier_b3_project_id)->toBeNull(); });