Files
portal/app/tests/Feature/AdminTenantsIndexTest.php
T
Дмитрий fa11c7b223 phase2(admin-tenants-mrr): mrr_rub в /api/admin/tenants (этап 7)
Закрывает gap из v1.66 — mock-форма имеет mrrRub, но API возвращал null.
Теперь AdminTenantsView показывает реальную колонку MRR.

Backend (AdminTenantsController::index):
- Добавлено tariff_plans.price_monthly as tariff_price_monthly в select.
- mrr_rub в response: price_monthly (string) если не-trial; иначе null.
- Aggregate-формат как у /admin/billing — string чтобы decimal не терял
  точность при передаче через JSON.

Pest +3 (AdminTenantsIndexTest):
- mrr_rub='990.00' для активного тарифа не-trial.
- mrr_rub=null для trial (даже если тариф есть).
- mrr_rub=null если current_tariff_id отсутствует.

Frontend:
- ApiAdminTenant.mrr_rub: string | null в типе.
- mapApiAdminTenant: parseFloat(api.mrr_rub) или null (вместо hardcoded
  null из v1.66).
- AdminTenantsView: formatRub(item.mrrRub) для консистентности с другими
  ₽-полями.

Vitest +2:
- mrr_rub строка → number.
- mrr_rub=null → mrrRub null.

PHPStan baseline регенерирован. cspell-glossary +консистентности.

Регресс:
- Lint+type-check+format passed.
- Vitest 313/313 за 18.83 сек (+2 от 311).
- Vite build 947 ms.
- Pint + PHPStan passed.
- Pest 266/266 за 28.39 сек (+3 от 263, 1001 assertion).

Реестр v1.70→v1.71 / CLAUDE.md v1.61→v1.62.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:08:12 +03:00

187 lines
7.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
beforeEach(function () {
// Чистим всё что было в seed/предыдущих тестах — для чистых счётчиков stats.
DB::table('tenants')->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);
});