Files
portal/app/tests/Feature/Project/ProjectSyncStatusFromPivotTest.php
T

204 lines
8.1 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 Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
/**
* Прод-баг 29.07.2026: карточка проекта показывала «Готовим к запуску» (жёлтое)
* у проектов, чей заказ у поставщика реально стоит. 5 из 9 работающих проектов
* на бою (t2 ×3, t9, t10), самый старый врал 13 дней.
*
* Причина: aggregateSyncStatus() читал ТОЛЬКО три legacy-колонки
* supplier_b{1,2,3}_project_id, а ночной SyncSupplierProjectsJob (единственный,
* кто в режиме batch реально заводит заказ) пишет ТОЛЬКО в pivot
* project_supplier_links и этих колонок не касается вообще.
*
* Здесь фиксируем: источник истины для статуса — pivot; legacy-колонки читаются
* дополнительно (объединение), чтобы не потерять проекты, у которых заполнены
* только они (handleBatch пишет колонку без pivot-строки).
*/
function spOk(string $platform, string $key, string $status = 'ok', ?string $syncedAt = '2026-07-28 15:05:00'): SupplierProject
{
return SupplierProject::query()->create([
'platform' => $platform,
'signal_type' => 'call',
'unique_key' => $key,
'subject_code' => null,
'current_limit' => 7,
'sync_status' => $status,
'last_synced_at' => $syncedAt,
]);
}
function linkPivot(Project $project, SupplierProject $sp): void
{
DB::table('project_supplier_links')->insertOrIgnore([
'project_id' => $project->id,
'supplier_project_id' => $sp->id,
'platform' => $sp->platform,
'subject_code' => null,
]);
}
it('reports ok when the order exists in the pivot and legacy columns are empty', function (): void {
$project = Project::factory()->create([
'signal_type' => 'call',
'signal_identifier' => '79990000001',
'supplier_b1_project_id' => null,
'supplier_b2_project_id' => null,
'supplier_b3_project_id' => null,
]);
DB::table('project_supplier_links')->where('project_id', $project->id)->delete();
foreach (['B1', 'B2', 'B3'] as $platform) {
linkPivot($project, spOk($platform, '79990000001'));
}
// Состояние, до которого доехал прод: колонки пустые, связки есть.
expect($project->fresh()->supplier_b1_project_id)->toBeNull()
->and(DB::table('project_supplier_links')->where('project_id', $project->id)->count())->toBe(3);
expect($project->fresh()->aggregateSyncStatus())->toBe('ok');
});
it('reports pending when a pivot-linked order is still pending', function (): void {
$project = Project::factory()->create([
'signal_type' => 'call',
'signal_identifier' => '79001112233',
'supplier_b1_project_id' => null,
'supplier_b2_project_id' => null,
'supplier_b3_project_id' => null,
]);
DB::table('project_supplier_links')->where('project_id', $project->id)->delete();
linkPivot($project, spOk('B1', '79001112233'));
linkPivot($project, spOk('B2', '79001112233', 'pending', null));
expect($project->fresh()->aggregateSyncStatus())->toBe('pending');
});
it('reports failed when any pivot-linked order failed', function (): void {
$project = Project::factory()->create([
'signal_type' => 'call',
'signal_identifier' => '79002223344',
'supplier_b1_project_id' => null,
'supplier_b2_project_id' => null,
'supplier_b3_project_id' => null,
]);
DB::table('project_supplier_links')->where('project_id', $project->id)->delete();
linkPivot($project, spOk('B1', '79002223344'));
linkPivot($project, spOk('B2', '79002223344', 'failed'));
expect($project->fresh()->aggregateSyncStatus())->toBe('failed');
});
it('still reports pending when there is no order at all', function (): void {
$project = Project::factory()->create([
'signal_type' => 'call',
'signal_identifier' => '79003334455',
'supplier_b1_project_id' => null,
'supplier_b2_project_id' => null,
'supplier_b3_project_id' => null,
]);
DB::table('project_supplier_links')->where('project_id', $project->id)->delete();
expect($project->fresh()->aggregateSyncStatus())->toBe('pending');
});
it('still reports ok for legacy projects that only have the old columns filled', function (): void {
$sp = spOk('B1', '79004445566');
$project = Project::factory()->create([
'signal_type' => 'call',
'signal_identifier' => '79004445566',
'supplier_b1_project_id' => $sp->id,
]);
DB::table('project_supplier_links')->where('project_id', $project->id)->delete();
expect($project->fresh()->aggregateSyncStatus())->toBe('ok');
});
it('lists supplier links from the pivot when legacy columns are empty', function (): void {
$project = Project::factory()->create([
'signal_type' => 'call',
'signal_identifier' => '79005556677',
'supplier_b1_project_id' => null,
'supplier_b2_project_id' => null,
'supplier_b3_project_id' => null,
]);
DB::table('project_supplier_links')->where('project_id', $project->id)->delete();
foreach (['B1', 'B2', 'B3'] as $platform) {
linkPivot($project, spOk($platform, '79005556677'));
}
$links = $project->fresh()->getSupplierLinks();
expect($links)->toHaveCount(3)
->and(collect($links)->pluck('platform')->sort()->values()->all())->toBe(['b1', 'b2', 'b3'])
->and(collect($links)->pluck('sync_status')->unique()->all())->toBe(['ok']);
});
it('takes the earliest sync time across pivot-linked orders', function (): void {
$project = Project::factory()->create([
'signal_type' => 'call',
'signal_identifier' => '79006667788',
'supplier_b1_project_id' => null,
'supplier_b2_project_id' => null,
'supplier_b3_project_id' => null,
]);
DB::table('project_supplier_links')->where('project_id', $project->id)->delete();
linkPivot($project, spOk('B1', '79006667788', 'ok', '2026-07-28 15:05:00'));
linkPivot($project, spOk('B2', '79006667788', 'ok', '2026-07-26 15:05:00'));
expect((string) $project->fresh()->aggregateLastSyncedAt())->toStartWith('2026-07-26');
});
it('does not double-count an order linked both by pivot and by a legacy column', function (): void {
$sp = spOk('B1', '79007778899');
$project = Project::factory()->create([
'signal_type' => 'call',
'signal_identifier' => '79007778899',
'supplier_b1_project_id' => $sp->id,
]);
DB::table('project_supplier_links')->where('project_id', $project->id)->delete();
linkPivot($project, $sp);
expect($project->fresh()->getSupplierLinks())->toHaveCount(1);
});
it('shows the project as collecting leads over the API when only the pivot is filled', function (): void {
$tenant = Tenant::factory()->create();
$user = User::factory()->create(['tenant_id' => $tenant->id]);
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'signal_type' => 'call',
'signal_identifier' => '79008889900',
'is_active' => true,
'preflight_blocked_at' => null,
'supplier_b1_project_id' => null,
'supplier_b2_project_id' => null,
'supplier_b3_project_id' => null,
]);
DB::table('project_supplier_links')->where('project_id', $project->id)->delete();
foreach (['B1', 'B2', 'B3'] as $platform) {
linkPivot($project, spOk($platform, '79008889900'));
}
$response = $this->actingAs($user)->getJson('/api/projects');
$row = collect($response->json('data'))->firstWhere('id', $project->id);
expect($row['sync_status'])->toBe('ok');
});