fix(billing): единый источник runway — дашборд = биллинг (F3)
Дашборд считал «хватит на дни» от legacy balance_leads (≈0 для рублёвых тенантов) и расходился с биллингом. Введён общий RunwayCalculator; оба контроллера считают runway от affordable leads (рубли→лиды по тарифу, BalanceToLeadsConverter). Фронт DashboardView больше не режет число дней до 7 сегментов полосы. TDD: 4 Pest нового сервиса + обновлён DashboardSummary + 1 vitest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -316,21 +316,8 @@ class BillingController extends Controller
|
||||
*/
|
||||
private function runwayDays(Tenant $tenant, int $affordableLeads): ?int
|
||||
{
|
||||
if ($affordableLeads <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$leadsLast30Days = (int) DB::table('lead_charges')
|
||||
->where('tenant_id', $tenant->id)
|
||||
->where('charged_at', '>=', now()->subDays(30))
|
||||
->count();
|
||||
|
||||
if ($leadsLast30Days <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$avgPerDay = $leadsLast30Days / 30.0;
|
||||
|
||||
return max(0, (int) floor($affordableLeads / $avgPerDay));
|
||||
// F3 (17.06.2026): единый источник расчёта — RunwayCalculator (общий с дашбордом),
|
||||
// чтобы прогноз «хватит на дни» не расходился между биллингом и дашбордом.
|
||||
return app(\App\Services\Billing\RunwayCalculator::class)->daysLeft((int) $tenant->id, $affordableLeads);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,13 +103,21 @@ class DashboardController extends Controller
|
||||
->map(fn ($c) => (int) $c)
|
||||
->toArray();
|
||||
|
||||
// --- runway ---
|
||||
// runway опирается на приток за фиксированное 7-дневное окно,
|
||||
// независимо от выбранного range (для today/30d $curLeads — не 7-дневный).
|
||||
$leads7d = (clone $base())->whereBetween('received_at', [$now->subDays(7), $now])->count();
|
||||
$avgDaily = $leads7d / 7.0;
|
||||
$balanceLeads = (int) ($tenant->balance_leads ?? 0);
|
||||
$runwayDays = $avgDaily > 0 ? (int) floor($balanceLeads / $avgDaily) : 0;
|
||||
// --- runway (F3, 17.06.2026: единый источник с биллингом) ---
|
||||
// Раньше дашборд считал от legacy `balance_leads` (после Billing v2 ≈0
|
||||
// для рублёвых тенантов) → расходился с биллингом «0 дней ↔ N дней».
|
||||
// Теперь — affordable leads от рублёвого баланса по тарифу
|
||||
// (BalanceToLeadsConverter) + общий RunwayCalculator.
|
||||
$activeTiers = app(\App\Repositories\PricingTierRepository::class)
|
||||
->activeAt(\Carbon\Carbon::now('Europe/Moscow'));
|
||||
$conversion = app(\App\Services\Billing\BalanceToLeadsConverter::class)->convert(
|
||||
(string) $tenant->balance_rub,
|
||||
(int) ($tenant->delivered_in_month ?? 0),
|
||||
$activeTiers,
|
||||
);
|
||||
$affordableLeads = (int) $conversion['leads'];
|
||||
$runwayDays = app(\App\Services\Billing\RunwayCalculator::class)
|
||||
->daysLeft($tenantId, $affordableLeads) ?? 0;
|
||||
|
||||
return [
|
||||
'range' => $range,
|
||||
@@ -119,7 +127,7 @@ class DashboardController extends Controller
|
||||
'balance' => [
|
||||
'amount_rub' => (string) $tenant->balance_rub,
|
||||
'runway_days' => $runwayDays,
|
||||
'runway_leads' => $balanceLeads,
|
||||
'runway_leads' => $affordableLeads,
|
||||
],
|
||||
'activity' => ['points' => $points, 'labels' => $labels, 'max' => $axisMax],
|
||||
'funnel' => (object) $funnel,
|
||||
|
||||
Reference in New Issue
Block a user