264 lines
11 KiB
PHP
264 lines
11 KiB
PHP
<?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();
|
||
});
|