25088e4a33
GET /api/admin/supplier-integration/manual-queue — pending список (limit 100).
POST /manual-queue/{id}/resolve — оператор пометил, что вручную создал проект
на портале; reconcile через channel->listProjects() по (platform, signal_type,
unique_key), 409 если не найден.
ОТКЛОНЕНИЕ ОТ plan Step 10.3: план писал portal external_id прямо в
projects.supplier_b*_project_id (FK на local supplier_projects.id) — FK
violation. Resolve делает firstOrCreate local supplier_projects row с
verified external_id, в FK пишет local id.
Routes — в группе saas-admin (web.php, EnsureSaasAdmin стаб). Task 10 of 12.
Tests 4/4 (index pending / exclude resolved / resolve match / resolve 409).
Spec §4.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
131 lines
4.8 KiB
PHP
131 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);
|
|
|
|
function authAdmin(): User
|
|
{
|
|
// EnsureSaasAdmin — стаб (Sprint 3F): в testing пропускает всех без
|
|
// проверки роли. actingAs нужен только чтобы $request->user() в
|
|
// manualQueueResolve дал id для resolved_by_user_id.
|
|
$admin = User::factory()->create();
|
|
test()->actingAs($admin);
|
|
|
|
return $admin;
|
|
}
|
|
|
|
it('GET /api/admin/supplier-integration/manual-queue returns pending rows', function (): void {
|
|
authAdmin();
|
|
$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 = test()->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 {
|
|
authAdmin();
|
|
$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(),
|
|
]);
|
|
|
|
test()->getJson('/api/admin/supplier-integration/manual-queue')
|
|
->assertOk()->assertJsonCount(0, 'queue');
|
|
});
|
|
|
|
it('POST /resolve marks row resolved when listProjects matches', function (): void {
|
|
$admin = authAdmin();
|
|
$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);
|
|
|
|
test()->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 {
|
|
authAdmin();
|
|
$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);
|
|
|
|
test()->postJson("/api/admin/supplier-integration/manual-queue/{$row->id}/resolve")
|
|
->assertStatus(409);
|
|
|
|
expect($row->fresh()->status)->toBe('pending');
|
|
});
|