Files
portal/app/tests/Feature/AdminTenantShowTest.php
T

264 lines
11 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create([
'subdomain' => 'okna-moscow',
'organization_name' => 'Окна Москва ООО',
'contact_email' => 'admin@okna-moscow.ru',
'is_trial' => false,
'balance_rub' => 14250.00,
'balance_leads' => 5,
]);
});
test('GET /api/admin/tenants/{subdomain}: 404 unknown', function () {
$this->getJson('/api/admin/tenants/unknown-subdomain')->assertStatus(404);
});
test('GET /api/admin/tenants/{subdomain}: 404 для soft-deleted', function () {
DB::table('tenants')->where('id', $this->tenant->id)->update(['deleted_at' => Carbon::now()]);
$this->getJson("/api/admin/tenants/{$this->tenant->subdomain}")->assertStatus(404);
});
test('GET /api/admin/tenants/{subdomain}: возвращает базовые поля', function () {
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
$response->assertOk();
expect($response->json('tenant.subdomain'))->toBe('okna-moscow');
expect($response->json('tenant.organization_name'))->toBe('Окна Москва ООО');
expect($response->json('tenant.contact_email'))->toBe('admin@okna-moscow.ru');
expect($response->json('tenant.balance_rub'))->toBe('14250.00');
expect($response->json('tenant.balance_leads'))->toBe(5);
expect($response->json('tenant.is_trial'))->toBeFalse();
});
test('GET /api/admin/tenants/{subdomain}: response содержит 4 секции + metrics', function () {
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
$response->assertOk();
expect($response->json())->toHaveKey('users');
expect($response->json())->toHaveKey('projects');
expect($response->json())->toHaveKey('balance_history');
expect($response->json())->toHaveKey('activity');
expect($response->json())->toHaveKey('metrics');
expect($response->json('metrics'))->toHaveKey('leads_today');
expect($response->json('metrics'))->toHaveKey('avg_lead_cost_rub');
expect($response->json('metrics'))->toHaveKey('runway_days');
});
test('GET show: users возвращает свои + изоляция от чужих', function () {
User::factory()->create(['tenant_id' => $this->tenant->id, 'email' => 'mine@test.io']);
$other = Tenant::factory()->create(['subdomain' => 'other-co']);
User::factory()->create(['tenant_id' => $other->id, 'email' => 'foreign@test.io']);
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
$emails = array_column($response->json('users'), 'email');
expect($emails)->toContain('mine@test.io');
expect($emails)->not->toContain('foreign@test.io');
});
test('GET show: projects возвращает с suppliers_count + leads_today', function () {
$project = Project::factory()->create([
'tenant_id' => $this->tenant->id,
'name' => 'Натяжные потолки',
'tag' => 'natyazhnye-potolki',
'is_active' => true,
]);
// suppliers — глобальная таблица (без tenant_id), code UNIQUE.
$supplierIds = [];
foreach (range(1, 2) as $i) {
$supplierIds[] = DB::table('suppliers')->insertGetId([
'code' => 'b'.Str::random(6),
'name' => "Supplier {$i}",
'accepts_types' => '{websites}',
'cost_rub' => 100.00,
'channel' => 'sites',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
}
foreach ($supplierIds as $sid) {
DB::table('project_suppliers')->insert([
'project_id' => $project->id,
'supplier_id' => $sid,
'is_active' => true,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
}
// 2 deals сегодня для проекта (используем сегодняшний день — партиция точно есть).
foreach (range(1, 2) as $i) {
DB::table('deals')->insert([
'tenant_id' => $this->tenant->id,
'project_id' => $project->id,
'phone' => "+7999000000{$i}",
'status' => 'new',
'received_at' => Carbon::now(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
}
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
$projects = $response->json('projects');
expect($projects)->toHaveCount(1);
expect($projects[0]['name'])->toBe('Натяжные потолки');
expect($projects[0]['suppliers_count'])->toBe(2);
expect($projects[0]['leads_today'])->toBe(2);
});
test('GET show: balance_history возвращает свои tx + ORDER created_at DESC + LIMIT 30', function () {
foreach (range(1, 35) as $i) {
DB::table('balance_transactions')->insert([
'tenant_id' => $this->tenant->id,
'type' => $i % 3 === 0 ? 'topup' : 'lead_charge',
'amount_rub' => $i % 3 === 0 ? 1000 : -200,
'created_at' => Carbon::now()->subMinutes(35 - $i),
]);
}
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
$history = $response->json('balance_history');
expect($history)->toHaveCount(30); // LIMIT
// Самая свежая первая (created_at DESC).
expect($history[0]['type'])->toBeIn(['topup', 'lead_charge']);
});
test('GET show: balance_history изоляция (чужие tx не показываются)', function () {
DB::table('balance_transactions')->insert([
'tenant_id' => $this->tenant->id,
'type' => 'topup',
'amount_rub' => 999,
'description' => 'mine',
'created_at' => Carbon::now(),
]);
$other = Tenant::factory()->create();
DB::table('balance_transactions')->insert([
'tenant_id' => $other->id,
'type' => 'topup',
'amount_rub' => 555,
'description' => 'foreign',
'created_at' => Carbon::now(),
]);
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
$descriptions = array_column($response->json('balance_history'), 'description');
expect($descriptions)->toContain('mine');
expect($descriptions)->not->toContain('foreign');
});
test('GET show: activity возвращает с actor_email из users LEFT JOIN', function () {
$user = User::factory()->create(['tenant_id' => $this->tenant->id, 'email' => 'actor@test.io']);
DB::table('activity_log')->insert([
'tenant_id' => $this->tenant->id,
'user_id' => $user->id,
'deal_id' => 999,
'event' => 'deal.status_changed',
'context' => json_encode(['from' => 'new', 'to' => 'in_progress']),
'created_at' => Carbon::now(),
]);
DB::table('activity_log')->insert([
'tenant_id' => $this->tenant->id,
'user_id' => null, // системное событие
'deal_id' => 1000,
'event' => 'webhook.received',
'created_at' => Carbon::now()->subMinute(),
]);
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
$activity = $response->json('activity');
expect($activity)->toHaveCount(2);
$events = array_column($activity, 'event');
expect($events)->toContain('deal.status_changed');
expect($events)->toContain('webhook.received');
// Самая свежая первая.
expect($activity[0]['event'])->toBe('deal.status_changed');
expect($activity[0]['actor_email'])->toBe('actor@test.io');
expect($activity[1]['actor_email'])->toBeNull();
});
test('GET show: metrics.leads_today + this_week + this_month', function () {
$project = Project::factory()->create(['tenant_id' => $this->tenant->id]);
// 1 deal сегодня
DB::table('deals')->insert([
'tenant_id' => $this->tenant->id,
'project_id' => $project->id,
'phone' => '+79991111111',
'status' => 'new',
'received_at' => Carbon::now(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
expect($response->json('metrics.leads_today'))->toBe(1);
expect($response->json('metrics.leads_this_week'))->toBeGreaterThanOrEqual(1);
expect($response->json('metrics.leads_this_month'))->toBeGreaterThanOrEqual(1);
});
test('GET show: metrics.runway_days computed из baalance + spend', function () {
// 30000 ₽ списано за 30 дней → avg_daily = 1000.
// Баланс 14250 → runway ~ 14 дней.
DB::table('balance_transactions')->insert([
'tenant_id' => $this->tenant->id,
'type' => 'lead_charge',
'amount_rub' => -30000,
'created_at' => Carbon::now()->subDays(15),
]);
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
$runway = $response->json('metrics.runway_days');
expect($runway)->toBeGreaterThan(10);
expect($runway)->toBeLessThan(20);
});
test('GET show: tariff_name + mrr_rub когда есть current_tariff_id', function () {
$tariffId = DB::table('tariff_plans')->insertGetId([
'code' => 'team-'.Str::random(6),
'name' => 'Команда',
'billing_model' => 'monthly',
'price_monthly' => 990.00,
'is_active' => true,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
DB::table('tenants')->where('id', $this->tenant->id)->update(['current_tariff_id' => $tariffId]);
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
expect($response->json('tenant.tariff_name'))->toBe('Команда');
expect($response->json('tenant.mrr_rub'))->toBe('990.00');
});
test('GET show: mrr_rub null для trial-тенанта (даже если есть тариф)', function () {
$tariffId = DB::table('tariff_plans')->insertGetId([
'code' => 'team-tr-'.Str::random(6),
'name' => 'Команда',
'billing_model' => 'monthly',
'price_monthly' => 990.00,
'is_active' => true,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
DB::table('tenants')->where('id', $this->tenant->id)->update([
'is_trial' => true,
'current_tariff_id' => $tariffId,
]);
$response = $this->getJson("/api/admin/tenants/{$this->tenant->subdomain}");
expect($response->json('tenant.is_trial'))->toBeTrue();
expect($response->json('tenant.mrr_rub'))->toBeNull();
});