From 992e62fd2c8074e07069164b265f104cc6fc54d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 14 Jul 2026 09:14:00 +0300 Subject: [PATCH] =?UTF-8?q?fix(visitors):=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B0=20=C2=AB=D0=A7=D1=82=D0=BE=20=D1=81=D0=BC?= =?UTF-8?q?=D0=BE=D1=82=D1=80=D1=8F=D1=82=20=D0=B2=20=D0=BA=D0=B0=D0=B1?= =?UTF-8?q?=D0=B8=D0=BD=D0=B5=D1=82=D0=B5=C2=BB=20=D0=BF=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=BB=D0=B0=20=E2=80=94=20=D0=BD=D0=B5=D1=82=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=BD=D0=BA=D0=B8=20t.name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Запрос активности портала просил у tenants колонку name, которой не существует: в схеме она называется organization_name. База отвечала 42703 Undefined column, фронт показывал красную плашку «Не удалось загрузить данные» поверх страницы /admin/visitors. Остальные три запроса страницы работали, поэтому карточки рисовались пустыми, а не сломанными. Наружу поле по-прежнему отдаётся как name — контракт API и фронт не менялись. Добавлен тест на portal-эндпоинт. Из четырёх запросов страницы тестами были покрыты три; единственный непокрытый и оказался сломанным — тот же класс потери, что CsvReconcileJobTest: код едет, тест нет. Проверки: AdminVisitors 5/5, смежные Tracking 14/14, Pint чисто, Larastan 0 ошибок. Прод не тронут — выката не было. Co-Authored-By: Claude Opus 4.8 1M context --- .../Api/AdminVisitorsController.php | 4 ++-- app/tests/Feature/AdminVisitorsTest.php | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/app/Http/Controllers/Api/AdminVisitorsController.php b/app/app/Http/Controllers/Api/AdminVisitorsController.php index 6866c19d..3361611e 100644 --- a/app/app/Http/Controllers/Api/AdminVisitorsController.php +++ b/app/app/Http/Controllers/Api/AdminVisitorsController.php @@ -131,8 +131,8 @@ class AdminVisitorsController extends Controller ->join('tenants as t', 't.id', '=', 'v.tenant_id') ->whereBetween('v.last_seen_at', [$from, $to]) ->whereNotNull('v.tenant_id') - ->groupBy('t.id', 't.name', 't.subdomain') - ->select(['t.name', 't.subdomain', DB::raw('MAX(v.last_seen_at) AS last_seen_at')]) + ->groupBy('t.id', 't.organization_name', 't.subdomain') + ->select(['t.organization_name as name', 't.subdomain', DB::raw('MAX(v.last_seen_at) AS last_seen_at')]) ->orderByDesc('last_seen_at') ->limit(50) ->get(); diff --git a/app/tests/Feature/AdminVisitorsTest.php b/app/tests/Feature/AdminVisitorsTest.php index 0cbf664b..40dc3baa 100644 --- a/app/tests/Feature/AdminVisitorsTest.php +++ b/app/tests/Feature/AdminVisitorsTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Models\Tenant; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -85,3 +86,24 @@ test('пустой период — нули, а не ошибка', function () $r->assertStatus(200); expect($r->json('funnel.view'))->toBe(0); }); + +test('активность в кабинете: экраны и клиенты, привязанные к гостю', function () { + $tenant = Tenant::factory()->create(['organization_name' => 'ООО Ромашка']); + + $id = visitor('sms', true, ['view', 'alive', 'login_done']); + DB::table('site_visitors')->where('id', $id)->update(['tenant_id' => $tenant->id]); + DB::table('site_events')->insert([ + ['visitor_id' => $id, 'event' => 'portal_screen', 'screen' => '/deals', 'occurred_at' => now(), 'meta' => '{}'], + ['visitor_id' => $id, 'event' => 'portal_screen', 'screen' => '/deals', 'occurred_at' => now(), 'meta' => '{}'], + ['visitor_id' => $id, 'event' => 'portal_screen', 'screen' => '/billing', 'occurred_at' => now(), 'meta' => '{}'], + ]); + + $r = $this->getJson('/api/admin/visitors/portal?period=7d'); + $r->assertStatus(200); + + $deals = collect($r->json('screens'))->firstWhere('screen', '/deals'); + expect($deals['opens'])->toBe(2); + expect($deals['people'])->toBe(1); + + expect(collect($r->json('clients'))->pluck('name'))->toContain('ООО Ромашка'); +});