992e62fd2c
Запрос активности портала просил у 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 <noreply@anthropic.com>
110 lines
4.2 KiB
PHP
110 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Tenant;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Tests\Concerns\SharesAdminPdo;
|
|
use Tests\Concerns\SharesSupplierPdo;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
uses(SharesSupplierPdo::class);
|
|
uses(SharesAdminPdo::class);
|
|
|
|
/** Гость + его шаги. */
|
|
function visitor(string $channel, bool $human, array $events, string $device = 'phone'): string
|
|
{
|
|
$id = (string) Str::uuid();
|
|
DB::table('site_visitors')->insert([
|
|
'id' => $id, 'first_seen_at' => now(), 'last_seen_at' => now(),
|
|
'channel' => $channel, 'device' => $device, 'is_human' => $human, 'is_datacenter' => false,
|
|
]);
|
|
foreach ($events as $e) {
|
|
DB::table('site_events')->insert([
|
|
'visitor_id' => $id, 'event' => $e, 'occurred_at' => now(), 'meta' => '{}',
|
|
]);
|
|
}
|
|
|
|
return $id;
|
|
}
|
|
|
|
beforeEach(function () {
|
|
DB::table('site_events')->delete();
|
|
DB::table('site_visitors')->delete();
|
|
});
|
|
|
|
test('воронка считает шаги и конверсии', function () {
|
|
visitor('sms', true, ['view', 'alive']);
|
|
visitor('sms', true, ['view', 'alive', 'cta_click', 'register_open', 'register_done', 'login_done']);
|
|
visitor('direct', false, ['view']); // браузер, но не человек
|
|
|
|
$r = $this->getJson('/api/admin/visitors/funnel?period=7d');
|
|
$r->assertStatus(200);
|
|
|
|
expect($r->json('funnel.view'))->toBe(3);
|
|
expect($r->json('funnel.alive'))->toBe(2);
|
|
expect($r->json('funnel.cta_click'))->toBe(1);
|
|
expect($r->json('funnel.register_done'))->toBe(1);
|
|
expect($r->json('funnel.login_done'))->toBe(1);
|
|
});
|
|
|
|
test('таблица каналов группирует людей по источнику', function () {
|
|
visitor('sms', true, ['view', 'alive']);
|
|
visitor('sms', true, ['view', 'alive', 'cta_click']);
|
|
visitor('hh', true, ['view', 'alive', 'cta_click', 'register_done']);
|
|
|
|
$r = $this->getJson('/api/admin/visitors/channels?period=7d');
|
|
$r->assertStatus(200);
|
|
|
|
$sms = collect($r->json('channels'))->firstWhere('channel', 'sms');
|
|
expect($sms['people'])->toBe(2);
|
|
expect($sms['cta_clicks'])->toBe(1);
|
|
expect($sms['registrations'])->toBe(0);
|
|
|
|
$hh = collect($r->json('channels'))->firstWhere('channel', 'hh');
|
|
expect($hh['registrations'])->toBe(1);
|
|
});
|
|
|
|
test('список визитов отдаёт цепочку шагов и пометку VPN', function () {
|
|
$id = visitor('direct', true, ['view', 'alive', 'cta_click']);
|
|
DB::table('site_visitors')->where('id', $id)->update([
|
|
'is_datacenter' => true, 'city' => 'Amsterdam', 'asn_org' => 'NordVPN', 'ip' => '1.2.3.4',
|
|
]);
|
|
|
|
$r = $this->getJson('/api/admin/visitors/visits?period=7d');
|
|
$r->assertStatus(200);
|
|
|
|
$v = $r->json('visits.0');
|
|
expect($v['is_datacenter'])->toBeTrue();
|
|
expect($v['steps'])->toContain('cta_click');
|
|
});
|
|
|
|
test('пустой период — нули, а не ошибка', function () {
|
|
$r = $this->getJson('/api/admin/visitors/funnel?period=today');
|
|
$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('ООО Ромашка');
|
|
});
|