delete(); DB::table('tenants')->delete(); // tariff_plans оставляем (seeded). }); function attachTariff(int $tenantId, string $name = 'Команда', float $monthly = 990.00): int { $tariffId = (int) DB::table('tariff_plans')->insertGetId([ 'code' => 'tp_'.bin2hex(random_bytes(4)), 'name' => $name, 'billing_model' => 'monthly', 'price_monthly' => $monthly, 'price_per_lead' => 10.00, 'included_leads' => 100, 'is_active' => true, 'is_public' => true, 'sort_order' => 1, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('tenants')->where('id', $tenantId)->update(['current_tariff_id' => $tariffId]); return $tariffId; } function makeBalanceTx(int $tenantId, string $type, float $amount, ?string $createdAt = null): void { DB::table('balance_transactions')->insert([ 'tenant_id' => $tenantId, 'type' => $type, 'amount_rub' => $amount, 'amount_leads' => 0, 'created_at' => $createdAt ?? now(), ]); } test('GET /api/admin/billing 200 + пустой', function () { $r = $this->getJson('/api/admin/billing'); $r->assertStatus(200); expect($r->json('tenants'))->toBe([]); expect($r->json('summary.total_mrr_rub'))->toBe('0'); expect($r->json('summary.overdue_count'))->toBe(0); expect($r->json('summary.refunds_count_30d'))->toBe(0); }); test('GET /api/admin/billing возвращает поля + tariff JOIN', function () { $tenant = Tenant::factory()->create([ 'organization_name' => 'Окна Москва', 'subdomain' => 'okna', 'balance_rub' => '14250.00', 'is_trial' => false, ]); attachTariff($tenant->id, 'Команда', 990.00); $r = $this->getJson('/api/admin/billing'); $row = $r->json('tenants.0'); expect($row['organization_name'])->toBe('Окна Москва'); expect($row['balance_rub'])->toBe('14250.00'); expect($row['tariff_name'])->toBe('Команда'); expect($row['mrr_rub'])->toBe('990.00'); }); test('GET /api/admin/billing аггрегирует topups + charges за текущий месяц', function () { $tenant = Tenant::factory()->create(); attachTariff($tenant->id); makeBalanceTx($tenant->id, 'topup', 5000.00); // ✓ in month makeBalanceTx($tenant->id, 'topup', 3000.00); // ✓ in month makeBalanceTx($tenant->id, 'lead_charge', -1850.00); // ABS = 1850 makeBalanceTx($tenant->id, 'lead_charge', -2400.00); $r = $this->getJson('/api/admin/billing'); $row = $r->json('tenants.0'); expect((float) $row['monthly_topups_rub'])->toBe(8000.0); expect((float) $row['monthly_charges_rub'])->toBe(4250.0); expect($row['last_payment_at'])->toBeString(); }); test('GET /api/admin/billing НЕ включает транзакции прошлого месяца в monthly aggregates', function () { $tenant = Tenant::factory()->create(); attachTariff($tenant->id); makeBalanceTx($tenant->id, 'topup', 5000.00); // в этом месяце makeBalanceTx($tenant->id, 'topup', 99999.00, now()->subMonths(2)->toDateTimeString()); // 2 месяца назад $r = $this->getJson('/api/admin/billing'); expect((float) $r->json('tenants.0.monthly_topups_rub'))->toBe(5000.0); }); test('GET /api/admin/billing summary считает overdue (balance<0 OR chargeback>0)', function () { Tenant::factory()->create(['balance_rub' => '100.00', 'chargeback_unrecovered_rub' => '0.00']); Tenant::factory()->create(['balance_rub' => '-200.00']); // overdue 1 Tenant::factory()->create(['balance_rub' => '500.00', 'chargeback_unrecovered_rub' => '300.00']); // overdue 2 $r = $this->getJson('/api/admin/billing'); expect($r->json('summary.overdue_count'))->toBe(2); }); test('GET /api/admin/billing summary считает refunds за 30 дней', function () { $tenant = Tenant::factory()->create(); makeBalanceTx($tenant->id, 'refund', -100.00); // ✓ <30 days makeBalanceTx($tenant->id, 'refund', -200.00); // ✓ makeBalanceTx($tenant->id, 'refund', -1000.00, now()->subDays(45)->toDateTimeString()); // > 30 days makeBalanceTx($tenant->id, 'topup', 500.00); // не refund $r = $this->getJson('/api/admin/billing'); expect($r->json('summary.refunds_count_30d'))->toBe(2); }); test('GET /api/admin/billing summary total_mrr суммирует tariff цен для не-trial тенантов', function () { $a = Tenant::factory()->create(['is_trial' => false]); attachTariff($a->id, 'Команда', 990.00); $b = Tenant::factory()->create(['is_trial' => false]); attachTariff($b->id, 'Pro', 4990.00); $c = Tenant::factory()->create(['is_trial' => true]); attachTariff($c->id, 'Команда', 990.00); // trial — не считается $r = $this->getJson('/api/admin/billing'); expect((float) $r->json('summary.total_mrr_rub'))->toBe(5980.0); // 990 + 4990 }); test('GET /api/admin/billing search ILIKE по name + subdomain', function () { Tenant::factory()->create(['organization_name' => 'Окна Москва', 'subdomain' => 'okna']); Tenant::factory()->create(['organization_name' => 'Двери СПб', 'subdomain' => 'dveri']); expect(count($this->getJson('/api/admin/billing?search=окна')->json('tenants')))->toBe(1); expect(count($this->getJson('/api/admin/billing?search=DVERI')->json('tenants')))->toBe(1); }); test('GET /api/admin/billing soft-deleted tenant скрыт', function () { $t = Tenant::factory()->create(['organization_name' => 'удалён']); $t->delete(); Tenant::factory()->create(['organization_name' => 'жив']); $r = $this->getJson('/api/admin/billing'); expect(count($r->json('tenants')))->toBe(1); expect($r->json('tenants.0.organization_name'))->toBe('жив'); });