31f0e86972
B1 из прогона тупой пользователь 24.06: RunwayCalculator считал от расхода за 30 дней, из-за чего на дашборде показывал 0 дней при полном балансе и раздувал число на короткой истории. Теперь считает от дневного заказа активных проектов requiredLeadsForTomorrow, как витрина ёмкости биллинга, и дашборд совпадает с биллингом. Тесты переписаны под новое поведение. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
3.5 KiB
PHP
77 lines
3.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Models\Project;
|
||
use App\Models\Tenant;
|
||
use App\Services\Billing\RunwayCalculator;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* B1-fix (24.06.2026): runway считается от ДНЕВНОГО ЗАКАЗА активных проектов
|
||
* (sum daily_limit_target), а не от прошлого расхода. Так дашборд перестаёт врать
|
||
* на новых аккаунтах («0 дней» при полном балансе) и совпадает с витриной биллинга.
|
||
* daysLeft(tenantId, affordableLeads):
|
||
* affordable<=0 → 0; нет активных проектов (заказ=0) → null;
|
||
* иначе floor(affordable / дневной_заказ).
|
||
*/
|
||
uses(DatabaseTransactions::class);
|
||
|
||
/** Создаёт активный проект тенанта с заданным дневным лимитом. */
|
||
function makeActiveProject(int $tenantId, int $dailyLimit): void
|
||
{
|
||
DB::statement('SET app.current_tenant_id = '.$tenantId);
|
||
Project::factory()->create([
|
||
'tenant_id' => $tenantId,
|
||
'is_active' => true,
|
||
'daily_limit_target' => $dailyLimit,
|
||
]);
|
||
}
|
||
|
||
test('daysLeft = 0 если affordableLeads <= 0', function () {
|
||
$tenant = Tenant::factory()->create();
|
||
makeActiveProject($tenant->id, 50);
|
||
expect(app(RunwayCalculator::class)->daysLeft($tenant->id, 0))->toBe(0);
|
||
expect(app(RunwayCalculator::class)->daysLeft($tenant->id, -5))->toBe(0);
|
||
});
|
||
|
||
test('daysLeft = null если нет активных проектов (нечего заказывать)', function () {
|
||
$tenant = Tenant::factory()->create();
|
||
expect(app(RunwayCalculator::class)->daysLeft($tenant->id, 100))->toBeNull();
|
||
});
|
||
|
||
test('daysLeft = floor(affordable / дневной заказ активных проектов)', function () {
|
||
$tenant = Tenant::factory()->create();
|
||
makeActiveProject($tenant->id, 50);
|
||
// 223 лида по карману, заказ 50/день → 4 полных дня (не «0» как раньше).
|
||
expect(app(RunwayCalculator::class)->daysLeft($tenant->id, 223))->toBe(4);
|
||
});
|
||
|
||
test('мало денег при большом заказе → 0 дней, а не раздутое число', function () {
|
||
$tenant = Tenant::factory()->create();
|
||
makeActiveProject($tenant->id, 50);
|
||
// 1 лид по карману, заказ 50/день → 0 дней (раньше выдавало «10 из 7»).
|
||
expect(app(RunwayCalculator::class)->daysLeft($tenant->id, 1))->toBe(0);
|
||
});
|
||
|
||
test('daysLeft суммирует лимиты нескольких активных проектов', function () {
|
||
$tenant = Tenant::factory()->create();
|
||
makeActiveProject($tenant->id, 30);
|
||
makeActiveProject($tenant->id, 20); // суммарный заказ 50/день
|
||
expect(app(RunwayCalculator::class)->daysLeft($tenant->id, 100))->toBe(2);
|
||
});
|
||
|
||
test('daysLeft игнорирует неактивные проекты', function () {
|
||
$tenant = Tenant::factory()->create();
|
||
makeActiveProject($tenant->id, 50);
|
||
DB::statement('SET app.current_tenant_id = '.$tenant->id);
|
||
Project::factory()->create([
|
||
'tenant_id' => $tenant->id,
|
||
'is_active' => false,
|
||
'daily_limit_target' => 1000,
|
||
]);
|
||
// считаем только активный (223/50=4), неактивный 1000 не учитывается.
|
||
expect(app(RunwayCalculator::class)->daysLeft($tenant->id, 223))->toBe(4);
|
||
});
|