refactor(billing-v2): runwayDays = affordable_leads ÷ avg-leads-per-day

This commit is contained in:
Дмитрий
2026-05-23 13:24:01 +03:00
parent 03e2dd2949
commit a4833a9ea4
2 changed files with 35 additions and 23 deletions
@@ -101,7 +101,7 @@ class BillingController extends Controller
'current_tier' => $conversion['current_tier'],
'next_tier' => $conversion['next_tier'],
'delivered_in_month' => (int) ($tenant->delivered_in_month ?? 0),
'runway_days' => $this->runwayDays($tenant),
'runway_days' => $this->runwayDays($tenant, $conversion['leads']),
'tiers_preview' => $tiersPreview,
'tariff' => $tenant->tariff === null ? null : [
'code' => $tenant->tariff->code,
@@ -186,27 +186,35 @@ class BillingController extends Controller
}
/**
* Прогноз «на сколько дней хватит баланса» оценочный UX-показатель.
* Прогноз «на сколько дней хватит affordable_leads» оценочный UX-показатель.
*
* = balance_rub / (рублёвые списания за 30 дней / 30). NULL, если списаний
* не было. Float здесь допустим: грубая оценка для шапки, НЕ мутация
* баланса (мутации баланса строго bcmath, см. BillingTopupService).
* Отрицательный баланс 0 (тенант уже в минусе, runway не может быть < 0).
* Billing v2 Spec A: считаем по affordable_leads (выход BalanceToLeadsConverter)
* делённому на среднюю скорость списания за 30 дней (count(lead_charges)/30).
* Раньше формула была balance_rub / per-day-rub-spend после унификации
* единицы измерения «лиды» более показательны и устраняют дрейф между
* рублёвой шапкой и тарифной ступенью.
*
* - affordable_leads 0 0 (тенант не может купить ни одного лида).
* - leadsLast30Days = 0 null (нет истории, не от чего считать).
* - иначе floor(affordable_leads / (leadsLast30Days / 30)).
*/
private function runwayDays(Tenant $tenant): ?int
private function runwayDays(Tenant $tenant, int $affordableLeads): ?int
{
$spent = abs((float) DB::table('balance_transactions')
->where('tenant_id', $tenant->id)
->where('type', BalanceTransaction::TYPE_LEAD_CHARGE)
->where('created_at', '>=', now()->subDays(30))
->sum('amount_rub'));
if ($affordableLeads <= 0) {
return 0;
}
if ($spent <= 0.0) {
$leadsLast30Days = (int) DB::table('lead_charges')
->where('tenant_id', $tenant->id)
->where('charged_at', '>=', now()->subDays(30))
->count();
if ($leadsLast30Days <= 0) {
return null;
}
$perDay = $spent / 30.0;
$avgPerDay = $leadsLast30Days / 30.0;
return max(0, (int) floor((float) $tenant->balance_rub / $perDay));
return max(0, (int) floor($affordableLeads / $avgPerDay));
}
}