delete(); }); test('GET /api/admin/tenants возвращает 200 и пустой список без данных', function () { $r = $this->getJson('/api/admin/tenants'); $r->assertStatus(200); expect($r->json('tenants'))->toBe([]); expect($r->json('total'))->toBe(0); expect($r->json('limit'))->toBe(100); expect($r->json('offset'))->toBe(0); }); test('GET /api/admin/tenants возвращает все поля', function () { $tenant = Tenant::factory()->create([ 'subdomain' => 'okna-msk', 'organization_name' => 'Окна Москва', 'contact_email' => 'admin@okna-msk.test', 'balance_rub' => '15000.00', 'balance_leads' => 287, 'is_trial' => false, 'desired_daily_numbers' => 12, ]); $r = $this->getJson('/api/admin/tenants'); $r->assertStatus(200); expect($r->json('total'))->toBe(1); $row = $r->json('tenants.0'); expect($row['id'])->toBe($tenant->id); expect($row['subdomain'])->toBe('okna-msk'); expect($row['organization_name'])->toBe('Окна Москва'); expect($row['contact_email'])->toBe('admin@okna-msk.test'); expect($row['balance_rub'])->toBe('15000.00'); expect($row['balance_leads'])->toBe(287); expect($row['is_trial'])->toBeFalse(); expect($row['desired_daily_numbers'])->toBe(12); }); test('GET /api/admin/tenants фильтрует по status', function () { Tenant::factory()->create(['status' => 'active', 'organization_name' => 'A']); Tenant::factory()->create(['status' => 'suspended', 'organization_name' => 'B']); Tenant::factory()->create(['status' => 'active', 'organization_name' => 'C']); $r = $this->getJson('/api/admin/tenants?status=active'); expect($r->json('total'))->toBe(2); $names = collect($r->json('tenants'))->pluck('organization_name')->all(); expect($names)->toContain('A'); expect($names)->toContain('C'); expect($names)->not->toContain('B'); }); test('GET /api/admin/tenants фильтрует по search (organization_name + subdomain + email ILIKE)', function () { Tenant::factory()->create(['organization_name' => 'Окна Москва', 'subdomain' => 'okna']); Tenant::factory()->create(['organization_name' => 'Двери СПб', 'subdomain' => 'dveri']); expect($this->getJson('/api/admin/tenants?search=окна')->json('total'))->toBe(1); expect($this->getJson('/api/admin/tenants?search=DVERI')->json('total'))->toBe(1); expect($this->getJson('/api/admin/tenants?search=что-то-несуществующее')->json('total'))->toBe(0); }); test('GET /api/admin/tenants сортирует по last_activity_at DESC', function () { $oldest = Tenant::factory()->create([ 'organization_name' => 'oldest', 'last_activity_at' => now()->subDays(5), ]); $newest = Tenant::factory()->create([ 'organization_name' => 'newest', 'last_activity_at' => now()->subMinutes(5), ]); $middle = Tenant::factory()->create([ 'organization_name' => 'middle', 'last_activity_at' => now()->subDays(1), ]); $r = $this->getJson('/api/admin/tenants'); $names = collect($r->json('tenants'))->pluck('organization_name')->all(); expect($names[0])->toBe('newest'); expect($names[1])->toBe('middle'); expect($names[2])->toBe('oldest'); }); test('GET /api/admin/tenants возвращает stats (total/active/trial/overdue)', function () { Tenant::factory()->create(['status' => 'active', 'is_trial' => false, 'balance_rub' => '100']); Tenant::factory()->create(['status' => 'active', 'is_trial' => true, 'balance_rub' => '50']); Tenant::factory()->create(['status' => 'suspended', 'is_trial' => false, 'balance_rub' => '-200']); Tenant::factory()->create(['status' => 'active', 'is_trial' => false, 'chargeback_unrecovered_rub' => '500']); $r = $this->getJson('/api/admin/tenants'); expect($r->json('stats.total'))->toBe(4); expect($r->json('stats.active'))->toBe(3); expect($r->json('stats.trial'))->toBe(1); expect($r->json('stats.overdue'))->toBe(2); // отрицательный balance + chargeback }); test('GET /api/admin/tenants soft-deleted tenant НЕ возвращается', function () { $deleted = Tenant::factory()->create(['organization_name' => 'удалён']); $deleted->delete(); // soft-delete Tenant::factory()->create(['organization_name' => 'жив']); $r = $this->getJson('/api/admin/tenants'); expect($r->json('total'))->toBe(1); expect($r->json('tenants.0.organization_name'))->toBe('жив'); }); test('GET /api/admin/tenants возвращает mrr_rub для активного тарифа (не-trial)', function () { $tariffId = (int) DB::table('tariff_plans')->insertGetId([ 'code' => 'tp_'.bin2hex(random_bytes(4)), 'name' => 'Команда', 'billing_model' => 'monthly', 'price_monthly' => 990.00, 'is_active' => true, 'is_public' => true, 'sort_order' => 1, 'created_at' => now(), ]); $tenant = Tenant::factory()->create([ 'organization_name' => 'Окна', 'is_trial' => false, ]); DB::table('tenants')->where('id', $tenant->id)->update(['current_tariff_id' => $tariffId]); $r = $this->getJson('/api/admin/tenants'); expect($r->json('tenants.0.mrr_rub'))->toBe('990.00'); }); test('GET /api/admin/tenants mrr_rub=null для trial', function () { $tariffId = (int) DB::table('tariff_plans')->insertGetId([ 'code' => 'tp_'.bin2hex(random_bytes(4)), 'name' => 'Команда', 'billing_model' => 'monthly', 'price_monthly' => 990.00, 'is_active' => true, 'is_public' => true, 'sort_order' => 1, 'created_at' => now(), ]); $tenant = Tenant::factory()->create([ 'organization_name' => 'Trial', 'is_trial' => true, ]); DB::table('tenants')->where('id', $tenant->id)->update(['current_tariff_id' => $tariffId]); $r = $this->getJson('/api/admin/tenants'); expect($r->json('tenants.0.mrr_rub'))->toBeNull(); }); test('GET /api/admin/tenants mrr_rub=null если current_tariff_id отсутствует', function () { Tenant::factory()->create(['organization_name' => 'NoTariff', 'current_tariff_id' => null]); $r = $this->getJson('/api/admin/tenants'); expect($r->json('tenants.0.mrr_rub'))->toBeNull(); }); test('GET /api/admin/tenants поддерживает limit + offset', function () { foreach (range(1, 5) as $i) { Tenant::factory()->create([ 'organization_name' => 'T'.$i, 'last_activity_at' => now()->subMinutes($i), ]); } $r = $this->getJson('/api/admin/tenants?limit=2&offset=1'); expect($r->json('total'))->toBe(5); expect($r->json('limit'))->toBe(2); expect($r->json('offset'))->toBe(1); expect(count($r->json('tenants')))->toBe(2); });