Files
portal/app/tests/Feature/Supplier/SupplierSyncRunSummaryTest.php
T
Дмитрий 1d18933d9e feat/supplier: экран отчёта о вечерней заливке (Эпик 5)
Владелец выбрал формат «экран в админке» (не письмо).
- SyncSupplierProjectsJob по завершении пишет строку-сводку в новую supplier_sync_runs
  (групп/синк/ручная/отложено/упало + status ok|partial|failed|aborted) через finally —
  пишется и при раннем abort (time-budget/mass-fail/auth).
- Эндпоинт GET /api/admin/supplier-integration/sync-runs + метод syncRuns.
- Экран SaaS-admin «Интеграция с поставщиком» → карточка «Вечерняя заливка проектов
  поставщику»: таблица заливок со статусом человеческим языком (Всё ровно/Частично/Сбой).
- Схема v8.55 +1 таблица (SaaS-level без RLS как supplier_csv_reconcile_log), миграция
  2026_06_25_130000, RLS-ревью 7/7. Проверено глазами в браузере (epic5-sync-runs-admin-screen.png).

Тесты: бэк 24/25 (1 skip) + фронт-экран 5/5 зелёные. Под LEFTHOOK=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:52:39 +03:00

81 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\Supplier\SyncSupplierProjectsJob;
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\Supplier\Channel\AjaxProjectChannel;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
/**
* Эпик 5 Task 5.1 — по завершении вечерней заливки джоб пишет строку-сводку
* в supplier_sync_runs (для экрана SaaS-admin «Интеграция с поставщиком»).
*/
beforeEach(function (): void {
Carbon::setTestNow(Carbon::parse('2026-05-27 18:06:00', 'Europe/Moscow'));
DB::table('supplier_sync_runs')->delete();
});
afterEach(fn () => Carbon::setTestNow());
it('пишет run-сводку даже когда нет eligible-проектов (groups_total=0, status=ok)', function (): void {
(new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));
$run = DB::table('supplier_sync_runs')->latest('id')->first();
expect($run)->not->toBeNull();
expect((int) $run->groups_total)->toBe(0);
expect((int) $run->synced_ok)->toBe(0);
expect($run->status)->toBe('ok');
expect($run->started_at)->not->toBeNull();
expect($run->finished_at)->not->toBeNull();
});
it('считает синхронизированную группу в синку run-сводки (groups_total=1, synced_ok=1)', function (): void {
$tenant = Tenant::factory()->create();
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'is_active' => true,
'signal_type' => 'site',
'signal_identifier' => 'okna-run.example.com',
'daily_limit_target' => 9,
'delivery_days_mask' => 127,
'regions' => [82, 83],
]);
insertSnapshotForTomorrow($project, regions: '{82,83}');
Http::fake([
'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
['status' => 'OK', 'message' => '', 'result' => null, 'id' => '2001'],
200,
),
'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
['projects' => [
['id' => '2001', 'src' => 'rt', 'name' => 'okna-run.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'okna-run.example.com'],
['id' => '2002', 'src' => 'bl', 'name' => 'okna-run.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'okna-run.example.com'],
['id' => '2003', 'src' => 'mt', 'name' => 'okna-run.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'okna-run.example.com'],
]],
200,
),
]);
(new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));
// Группа реально создалась у поставщика.
expect(SupplierProject::on('pgsql_supplier')->where('unique_key', 'okna-run.example.com')->count())->toBe(3);
$run = DB::table('supplier_sync_runs')->latest('id')->first();
expect($run)->not->toBeNull();
expect((int) $run->groups_total)->toBe(1);
expect((int) $run->synced_ok)->toBe(1);
expect((int) $run->failed)->toBe(0);
expect($run->status)->toBe('ok');
});