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

264 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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();
});