191 lines
6.8 KiB
PHP
191 lines
6.8 KiB
PHP
|
|
<?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 — не удалён локально
|
|||
|
|
});
|