Files
portal/app/tests/Feature/Admin/SupplierProjectsAdminTest.php
T

191 lines
6.8 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Supplier\SupplierPortalClient;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
// EnsureSaasAdmin — стаб в testing (см. SupplierManualQueueTest::16): обычный
// User::factory + actingAs без guard'а.
it('GET /admin/supplier-integration/projects returns rows with orderers + last delivery', function (): void {
$this->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<int, int> */
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 — не удалён локально
});