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); });