user() в manualQueueResolve дал // id для resolved_by_user_id. it('GET /api/admin/supplier-integration/manual-queue returns pending rows', function (): void { $this->actingAs(User::factory()->create()); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->create(); SupplierManualSyncQueue::create([ 'project_id' => $project->id, 'platform' => 'B1', 'operation' => 'create', 'payload_snapshot' => ['limit' => 10], 'failure_reason' => 'contract_break', 'status' => 'pending', ]); $r = $this->getJson('/api/admin/supplier-integration/manual-queue'); $r->assertOk() ->assertJsonStructure(['queue' => [['id', 'project_id', 'platform', 'operation', 'payload_snapshot', 'failure_reason', 'created_at']]]) ->assertJsonCount(1, 'queue'); }); it('GET excludes resolved rows', function (): void { $this->actingAs(User::factory()->create()); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->create(); SupplierManualSyncQueue::create([ 'project_id' => $project->id, 'platform' => 'B1', 'operation' => 'create', 'payload_snapshot' => [], 'failure_reason' => 'contract_break', 'status' => 'resolved', 'resolved_at' => now(), ]); $this->getJson('/api/admin/supplier-integration/manual-queue') ->assertOk()->assertJsonCount(0, 'queue'); }); it('POST /resolve marks row resolved when listProjects matches', function (): void { $admin = User::factory()->create(); $this->actingAs($admin); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->create(); $row = SupplierManualSyncQueue::create([ 'project_id' => $project->id, 'platform' => 'B1', 'operation' => 'create', 'payload_snapshot' => ['signal_type' => 'site', 'unique_key' => 'foo.com'], 'failure_reason' => 'contract_break', 'status' => 'pending', ]); $channelMock = new class implements SupplierProjectChannel { public function createProject(SupplierProjectDto $dto): int { return 0; } public function updateProject(int $externalId, SupplierProjectDto $dto): void {} public function listProjects(): array { return [['id' => 99999, 'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'foo.com']]; } }; app()->instance(SupplierProjectChannel::class, $channelMock); $this->postJson("/api/admin/supplier-integration/manual-queue/{$row->id}/resolve") ->assertOk(); expect($row->fresh()->status)->toBe('resolved'); expect($row->fresh()->resolved_by_user_id)->toBe($admin->id); // FK ведёт на local supplier_projects.id; portal external_id (99999) хранится // в supplier_external_id созданной строки + в queue-row.external_id. expect($project->fresh()->supplier_b1_project_id)->not->toBeNull(); expect(SupplierProject::find($project->fresh()->supplier_b1_project_id)->supplier_external_id)->toBe('99999'); expect($row->fresh()->external_id)->toBe('99999'); }); it('POST /resolve returns 409 when listProjects does not match', function (): void { $this->actingAs(User::factory()->create()); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->create(); $row = SupplierManualSyncQueue::create([ 'project_id' => $project->id, 'platform' => 'B1', 'operation' => 'create', 'payload_snapshot' => ['signal_type' => 'site', 'unique_key' => 'foo.com'], 'failure_reason' => 'contract_break', 'status' => 'pending', ]); $channelMock = new class implements SupplierProjectChannel { public function createProject(SupplierProjectDto $dto): int { return 0; } public function updateProject(int $externalId, SupplierProjectDto $dto): void {} public function listProjects(): array { return []; } }; app()->instance(SupplierProjectChannel::class, $channelMock); $this->postJson("/api/admin/supplier-integration/manual-queue/{$row->id}/resolve") ->assertStatus(409); expect($row->fresh()->status)->toBe('pending'); });