create([ 'tenant_id' => $tenant->id, 'project_id' => $project->id, 'status' => $status, 'received_at' => $receivedAt, 'deleted_at' => $deletedAt, 'is_test' => $isTest, ]); } /** Авторизоваться как пользователь данного тенанта (auth:sanctum + tenant). */ function actingForTenant(Tenant $tenant): void { test()->actingAs(User::factory()->for($tenant)->create()); } it('401 без авторизации', function () { $this->getJson('/api/dashboard/summary')->assertStatus(401); }); it('возвращает структуру summary с range по умолчанию 7d', function () { $tenant = Tenant::factory()->create([ 'limits' => ['max_projects' => 10], 'balance_rub' => '14250.00', 'balance_leads' => 285, ]); actingForTenant($tenant); $this->getJson('/api/dashboard/summary') ->assertOk() ->assertJsonPath('range', '7d') ->assertJsonPath('balance.amount_rub', '14250.00') ->assertJsonStructure([ 'range', 'leads_received' => ['value', 'delta_pct', 'delta_dir'], 'conversion' => ['value', 'delta_pp', 'delta_dir'], 'active_projects' => ['active', 'limit'], 'balance' => ['amount_rub', 'runway_days', 'runway_leads'], 'activity' => ['points', 'labels', 'max'], 'funnel', ]); }); it('leads_received считает только сделки окна, без deleted и is_test', function () { $tenant = Tenant::factory()->create(); actingForTenant($tenant); $project = Project::factory()->create(['tenant_id' => $tenant->id]); // 3 живые сделки в окне 7d + 1 deleted + 1 is_test + 1 вне окна (8 дней назад) makeDashboardDeal($tenant, $project, 'new', now()->subDays(1)); makeDashboardDeal($tenant, $project, 'new', now()->subDays(2)); makeDashboardDeal($tenant, $project, 'won', now()->subDays(3)); makeDashboardDeal($tenant, $project, 'new', now()->subDays(1), deletedAt: now()); makeDashboardDeal($tenant, $project, 'new', now()->subDays(1), isTest: true); makeDashboardDeal($tenant, $project, 'new', now()->subDays(8)); $this->getJson('/api/dashboard/summary?range=7d') ->assertOk() ->assertJsonPath('leads_received.value', 3); }); it('conversion = доля статуса won в окне', function () { $tenant = Tenant::factory()->create(); actingForTenant($tenant); $project = Project::factory()->create(['tenant_id' => $tenant->id]); makeDashboardDeal($tenant, $project, 'won', now()->subDays(1)); makeDashboardDeal($tenant, $project, 'new', now()->subDays(1)); makeDashboardDeal($tenant, $project, 'new', now()->subDays(1)); makeDashboardDeal($tenant, $project, 'new', now()->subDays(1)); // 1 won из 4 → 25.0%; PHP json_encode кодирует 25.0 как 25 (без дроби) $this->getJson('/api/dashboard/summary') ->assertOk() ->assertJsonPath('conversion.value', 25); }); it('active_projects считает is_active=true + limit из limits', function () { $tenant = Tenant::factory()->create(['limits' => ['max_projects' => 10]]); actingForTenant($tenant); Project::factory()->create(['tenant_id' => $tenant->id, 'is_active' => true]); Project::factory()->create(['tenant_id' => $tenant->id, 'is_active' => true]); Project::factory()->create(['tenant_id' => $tenant->id, 'is_active' => false]); $this->getJson('/api/dashboard/summary') ->assertOk() ->assertJsonPath('active_projects.active', 2) ->assertJsonPath('active_projects.limit', 10); }); it('funnel группирует живые сделки по статусу', function () { $tenant = Tenant::factory()->create(); actingForTenant($tenant); $project = Project::factory()->create(['tenant_id' => $tenant->id]); makeDashboardDeal($tenant, $project, 'new', now()->subDays(1)); makeDashboardDeal($tenant, $project, 'new', now()->subDays(1)); makeDashboardDeal($tenant, $project, 'won', now()->subDays(1)); $this->getJson('/api/dashboard/summary') ->assertOk() ->assertJsonPath('funnel.new', 2) ->assertJsonPath('funnel.won', 1); }); it('activity возвращает 7 точек и 7 меток', function () { $tenant = Tenant::factory()->create(); actingForTenant($tenant); $this->getJson('/api/dashboard/summary') ->assertOk() ->assertJsonCount(7, 'activity.points') ->assertJsonCount(7, 'activity.labels'); }); it('runway_days использует фикс. 7д-окно независимо от range', function () { // balance_leads = 70; 7 сделок за последние 7 дней → avgDaily=1 → runway=70. // Баг: range=today → $curLeads=1 → avgDaily=1/7≈0.143 → runway≈490 (неверно). $tenant = Tenant::factory()->create(['balance_leads' => 70]); actingForTenant($tenant); $project = Project::factory()->create(['tenant_id' => $tenant->id]); for ($i = 0; $i <= 6; $i++) { makeDashboardDeal($tenant, $project, 'new', now()->subDays($i)); } $this->getJson('/api/dashboard/summary?range=today') ->assertOk() ->assertJsonPath('balance.runway_days', 70); });