insert([ 'actor_type' => 'tenant_user', 'tenant_id' => $tenantId, 'user_id' => $userId, 'email' => 'client@example.test', 'event' => $event, 'ip_address' => '5.166.40.65', 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'created_at' => CarbonImmutable::parse($mskTime, 'Europe/Moscow')->utc(), ]); } function alivenessTenant(): array { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); return [(int) $tenant->id, (int) $user->id]; } it('считает клиента живым, если он заходил не позже трёх суток назад', function () { [$tenantId, $userId] = alivenessTenant(); $now = CarbonImmutable::parse('2026-08-07 12:00:00', 'Europe/Moscow'); alivenessLog($tenantId, $userId, 'login_success', '2026-08-04 09:00:00'); $result = app(TenantAlivenessService::class)->forTenant($tenantId, $now); expect($result['state'])->toBe('active'); expect($result['days_since_login'])->toBe(3); }); it('считает клиента остывающим на четвёртые сутки', function () { [$tenantId, $userId] = alivenessTenant(); $now = CarbonImmutable::parse('2026-08-07 12:00:00', 'Europe/Moscow'); alivenessLog($tenantId, $userId, 'login_success', '2026-08-03 09:00:00'); $result = app(TenantAlivenessService::class)->forTenant($tenantId, $now); expect($result['state'])->toBe('cooling'); expect($result['days_since_login'])->toBe(4); }); it('считает клиента пропавшим на четырнадцатые сутки', function () { [$tenantId, $userId] = alivenessTenant(); $now = CarbonImmutable::parse('2026-08-07 12:00:00', 'Europe/Moscow'); alivenessLog($tenantId, $userId, 'login_success', '2026-07-24 09:00:00'); $result = app(TenantAlivenessService::class)->forTenant($tenantId, $now); expect($result['state'])->toBe('lost'); expect($result['days_since_login'])->toBe(14); }); it('не путает сутки в первые три часа ночи по Москве', function () { [$tenantId, $userId] = alivenessTenant(); // 01:30 МСК седьмого августа — это ещё 22:30 UTC шестого. // Наивная граница дала бы «вчера» вместо «сегодня». $now = CarbonImmutable::parse('2026-08-07 01:30:00', 'Europe/Moscow'); alivenessLog($tenantId, $userId, 'login_success', '2026-08-07 01:00:00'); $result = app(TenantAlivenessService::class)->forTenant($tenantId, $now); expect($result['days_since_login'])->toBe(0); expect($result['state'])->toBe('active'); }); it('отличает зарегистрировавшегося без единого входа', function () { [$tenantId, $userId] = alivenessTenant(); $now = CarbonImmutable::parse('2026-08-07 12:00:00', 'Europe/Moscow'); alivenessLog($tenantId, $userId, 'register_success', '2026-08-06 09:00:00'); $result = app(TenantAlivenessService::class)->forTenant($tenantId, $now); expect($result['state'])->toBe('never_logged_in'); }); it('отдаёт no_data, когда в журнале нет ни одной строки', function () { [$tenantId] = alivenessTenant(); $now = CarbonImmutable::parse('2026-08-07 12:00:00', 'Europe/Moscow'); $result = app(TenantAlivenessService::class)->forTenant($tenantId, $now); expect($result['state'])->toBe('no_data'); expect($result['last_login_at'])->toBeNull(); expect($result['days'])->toBe([]); });