6c30c248bc
FailoverProjectChannel: убран unreachable throw new LogicException после try-catch в createProjectForLiderra — все ветки уже терминируют (return / throw WindowDeferred / escalateToTier3(): never). phpstan deadCode.unreachable. SupplierManualQueueTest: test()-> заменён на $this-> (идиома проекта, как AdminPricingTiersControllerTest) — phpstan не типизирует Pest higher-order test(); authAdmin() helper убран, actingAs inline в it(). Изолированный phpstan по supplier-failover файлам — 0 реальных ошибок. Task 12 cleanup. Channel-тесты 7/7, admin-тесты 4/4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
4.8 KiB
PHP
125 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Project;
|
|
use App\Models\SupplierManualSyncQueue;
|
|
use App\Models\SupplierProject;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Supplier\Channel\SupplierProjectChannel;
|
|
use App\Services\Supplier\Dto\SupplierProjectDto;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
// EnsureSaasAdmin — стаб (Sprint 3F): в testing пропускает всех без проверки
|
|
// роли. actingAs нужен только чтобы $request->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');
|
|
});
|