actingAs(User::factory()->create()); $tenant = Tenant::factory()->create(['organization_name' => 'ООО Ромашка']); $sp = SupplierProject::query()->create([ 'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'okna.ru', 'subject_code' => 82, // Москва (по конституционному порядку, ст. 65) 'current_limit' => 5, 'current_workdays' => [1, 2, 3, 4, 5, 6, 7], 'current_regions' => null, 'sync_status' => 'ok', 'supplier_external_id' => '777', ]); $project = Project::factory()->for($tenant)->create(); DB::table('project_supplier_links')->insert([ 'project_id' => $project->id, 'supplier_project_id' => $sp->id, 'platform' => 'B1', 'subject_code' => 82, ]); DB::table('supplier_leads')->insert([ 'supplier_project_id' => $sp->id, 'platform' => 'B1', 'raw_payload' => json_encode([]), 'phone' => '+79991234567', 'received_at' => '2026-05-19 10:00:00', ]); $resp = $this->getJson('/api/admin/supplier-integration/projects') ->assertOk() ->json(); $row = collect($resp['projects'])->firstWhere('id', $sp->id); expect($row)->not->toBeNull() ->and($row['unique_key'])->toBe('okna.ru') ->and($row['subject_code'])->toBe(82) ->and($row['subject_name'])->toBe('Москва') ->and($row['platform'])->toBe('B1') ->and($row['current_limit'])->toBe(5) ->and($row['orderers'])->toContain('ООО Ромашка') ->and($row['last_delivery_at'])->not->toBeNull(); }); it('GET /projects returns subject_name «РФ» for NULL subject_code', function (): void { $this->actingAs(User::factory()->create()); $sp = SupplierProject::query()->create([ 'platform' => 'B2', 'signal_type' => 'site', 'unique_key' => 'all-russia.example', 'subject_code' => null, 'current_limit' => 0, 'current_workdays' => [1, 2, 3, 4, 5, 6, 7], 'current_regions' => null, 'sync_status' => 'ok', 'supplier_external_id' => '888', ]); $resp = $this->getJson('/api/admin/supplier-integration/projects')->assertOk()->json(); $row = collect($resp['projects'])->firstWhere('id', $sp->id); expect($row['subject_code'])->toBeNull() ->and($row['subject_name'])->toBe('РФ'); }); it('POST /projects/delete deletes on portal + locally (pivot cascades)', function (): void { $this->actingAs(User::factory()->create()); // Мокаем portal-клиент, чтобы не лезть в Redis-сессию (SupplierPortalClient::loadSession()). $deletedExternalIds = []; $clientMock = new class($deletedExternalIds) extends SupplierPortalClient { /** @var array */ public array $calls; public function __construct(array &$calls) { $this->calls = &$calls; } public function deleteProject(int $externalId): void { $this->calls[] = $externalId; } }; app()->instance(SupplierPortalClient::class, $clientMock); $tenant = Tenant::factory()->create(); $sp = SupplierProject::query()->create([ 'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'delete-me.ru', 'subject_code' => 77, 'current_limit' => 0, 'current_workdays' => [1, 2, 3, 4, 5, 6, 7], 'current_regions' => null, 'sync_status' => 'ok', 'supplier_external_id' => '999', ]); $project = Project::factory()->for($tenant)->create(); DB::table('project_supplier_links')->insert([ 'project_id' => $project->id, 'supplier_project_id' => $sp->id, 'platform' => 'B1', 'subject_code' => 77, ]); $this->postJson('/api/admin/supplier-integration/projects/delete', ['ids' => [$sp->id]]) ->assertOk() ->assertJson(['deleted' => 1, 'failures' => []]); expect(SupplierProject::find($sp->id))->toBeNull(); expect($clientMock->calls)->toBe([999]); expect(DB::table('project_supplier_links')->where('supplier_project_id', $sp->id)->count())->toBe(0); }); it('POST /projects/delete validates ids array', function (): void { $this->actingAs(User::factory()->create()); $this->postJson('/api/admin/supplier-integration/projects/delete', ['ids' => []]) ->assertStatus(422); $this->postJson('/api/admin/supplier-integration/projects/delete', []) ->assertStatus(422); }); it('POST /projects/delete collects failures without aborting batch', function (): void { $this->actingAs(User::factory()->create()); $clientMock = new class extends SupplierPortalClient { public int $callsCount = 0; public function __construct() {} public function deleteProject(int $externalId): void { $this->callsCount++; if ($externalId === 555) { throw new RuntimeException('portal said no'); } } }; app()->instance(SupplierPortalClient::class, $clientMock); $spOk = SupplierProject::query()->create([ 'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'ok.ru', 'subject_code' => 77, 'current_limit' => 0, 'current_workdays' => [1, 2, 3, 4, 5, 6, 7], 'current_regions' => null, 'sync_status' => 'ok', 'supplier_external_id' => '111', ]); $spBad = SupplierProject::query()->create([ 'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'bad.ru', 'subject_code' => 77, 'current_limit' => 0, 'current_workdays' => [1, 2, 3, 4, 5, 6, 7], 'current_regions' => null, 'sync_status' => 'ok', 'supplier_external_id' => '555', ]); $resp = $this->postJson('/api/admin/supplier-integration/projects/delete', [ 'ids' => [$spOk->id, $spBad->id], ])->assertOk()->json(); expect($resp['deleted'])->toBe(1) ->and(count($resp['failures']))->toBe(1) ->and($resp['failures'][0]['id'])->toBe($spBad->id) ->and($resp['failures'][0]['error'])->toContain('portal said no'); expect(SupplierProject::find($spOk->id))->toBeNull(); expect(SupplierProject::find($spBad->id))->not->toBeNull(); // bad — не удалён локально });