02a8a90e4d
Backend: AdminDashboardController +leads/+supply эндпоинты, summary дополнен плитками leads/supply; сверка заказа вынесена в чистый сервис SupplyReconciliation (спрос → формула computeOrder=max(max,⌈Σ/3⌉) → факт → рассинхрон). Лиды: доставлено сегодня / зависшие 4ч+ / нераспределённые / % доставки — cross-tenant под pgsql_admin. Frontend: плитки Лиды и Заказ оживлены (убраны заглушки «Этап 2»), drill с KPI и таблицей групп спрос→формула→факт→совпадает. Тесты: SupplyReconciliation unit 3/3, Leads/Supply/Summary feature, admin-срез 87 зелёных, фронт 10/10. stan 0, pint/eslint/type-check/build чисто. phpstan-baseline перегенерирован (getJson false-positive на новых тестах). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
44 lines
2.0 KiB
PHP
44 lines
2.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Dashboard\SupplyReconciliation;
|
|
|
|
it('считает формулу max(max, ceil(sum/3)) и помечает рассинхрон', function () {
|
|
// Группа A: спрос 100, max 100 → формула max(100, ceil(100/3)=34)=100, факт 100 → OK
|
|
// Группа B: спрос 60, max 30 → формула max(30, ceil(60/3)=20)=30, факт 35 → рассинхрон
|
|
$demand = [
|
|
['signal_type' => 'site', 'identifier' => 'a.ru', 'demand' => 100, 'max_limit' => 100],
|
|
['signal_type' => 'site', 'identifier' => 'b.ru', 'demand' => 60, 'max_limit' => 30],
|
|
];
|
|
$orderedByKey = ['site|a.ru' => 100, 'site|b.ru' => 35];
|
|
|
|
$result = SupplyReconciliation::build($demand, $orderedByKey);
|
|
|
|
expect($result['groups'])->toHaveCount(2);
|
|
expect($result['groups'][0])->toMatchArray([
|
|
'signal_type' => 'site', 'identifier' => 'a.ru',
|
|
'demand' => 100, 'formula' => 100, 'ordered' => 100, 'in_sync' => true,
|
|
]);
|
|
expect($result['groups'][1])->toMatchArray([
|
|
'identifier' => 'b.ru', 'demand' => 60, 'formula' => 30, 'ordered' => 35, 'in_sync' => false,
|
|
]);
|
|
expect($result['totals'])->toMatchArray([
|
|
'demand' => 160, 'formula' => 130, 'ordered' => 135, 'mismatches' => 1,
|
|
]);
|
|
});
|
|
|
|
it('факт 0 когда группы нет в supplier_projects → рассинхрон', function () {
|
|
$demand = [['signal_type' => 'call', 'identifier' => '79990001122', 'demand' => 10, 'max_limit' => 10]];
|
|
$result = SupplyReconciliation::build($demand, []);
|
|
expect($result['groups'][0]['ordered'])->toBe(0);
|
|
expect($result['groups'][0]['in_sync'])->toBeFalse();
|
|
expect($result['totals']['mismatches'])->toBe(1);
|
|
});
|
|
|
|
it('пустой спрос → пустые группы и нулевые итоги', function () {
|
|
$result = SupplyReconciliation::build([], []);
|
|
expect($result['groups'])->toBe([]);
|
|
expect($result['totals'])->toMatchArray(['demand' => 0, 'formula' => 0, 'ordered' => 0, 'mismatches' => 0]);
|
|
});
|