feat(billing-v2-c): BalancePreflightService — pure-проверка платёжеспособности
Task 1.2 Спека C. evaluate(balanceRub, deliveredInMonth, requiredLeads, tiers) →
PreflightResult{passes, requiredLeads, capacityLeads, deficitLeads}. Сравнение в
лидах через BalanceToLeadsConverter::convert (7 ступеней + месячный объём).
3 unit-теста GREEN. Pint passed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Billing;
|
||||
|
||||
use App\Models\PricingTier;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
/**
|
||||
* Pure: проходит ли клиент преfflight — хватает ли баланса на ПОЛНЫЙ дневной
|
||||
* лимит всех его eligible-проектов по текущему тарифу.
|
||||
*
|
||||
* Сравнение в ЛИДАХ (capacity vs required), не в рублях — переиспользует
|
||||
* BalanceToLeadsConverter::convert, который учитывает 7 ступеней и накопленный
|
||||
* месячный объём (deliveredInMonth).
|
||||
*
|
||||
* Spec: docs/superpowers/specs/2026-05-24-billing-v2-spec-c-preflight-vtb-design.md §3.3
|
||||
*/
|
||||
final class BalancePreflightService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BalanceToLeadsConverter $converter = new BalanceToLeadsConverter,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param Collection<int, PricingTier> $tiers
|
||||
*/
|
||||
public function evaluate(
|
||||
string $balanceRub,
|
||||
int $deliveredInMonth,
|
||||
int $requiredLeads,
|
||||
Collection $tiers,
|
||||
): PreflightResult {
|
||||
if ($requiredLeads <= 0) {
|
||||
return new PreflightResult(true, 0, 0, 0);
|
||||
}
|
||||
|
||||
$capacity = (int) $this->converter->convert($balanceRub, $deliveredInMonth, $tiers)['leads'];
|
||||
$passes = $capacity >= $requiredLeads;
|
||||
|
||||
return new PreflightResult(
|
||||
passes: $passes,
|
||||
requiredLeads: $requiredLeads,
|
||||
capacityLeads: $capacity,
|
||||
deficitLeads: $passes ? 0 : ($requiredLeads - $capacity),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Billing;
|
||||
|
||||
/**
|
||||
* Результат преfflight-проверки платёжеспособности тенанта (Billing v2 Spec C).
|
||||
*
|
||||
* Spec: docs/superpowers/specs/2026-05-24-billing-v2-spec-c-preflight-vtb-design.md §3.3
|
||||
*/
|
||||
final class PreflightResult
|
||||
{
|
||||
public function __construct(
|
||||
public readonly bool $passes,
|
||||
public readonly int $requiredLeads,
|
||||
public readonly int $capacityLeads,
|
||||
public readonly int $deficitLeads,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\PricingTier;
|
||||
use App\Services\Billing\BalancePreflightService;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
function makeTiers(): Collection
|
||||
{
|
||||
// 1 ступень: цена 50₽/лид (5000 копеек), безлимитная.
|
||||
return new Collection([
|
||||
new PricingTier(['tier_no' => 1, 'leads_in_tier' => null, 'price_per_lead_kopecks' => 5000, 'is_active' => true]),
|
||||
]);
|
||||
}
|
||||
|
||||
it('passes when balance covers full daily limit', function () {
|
||||
$service = new BalancePreflightService;
|
||||
// 2000₽ / 50₽ = 40 лидов capacity; нужно 30 → проходит.
|
||||
$result = $service->evaluate(
|
||||
balanceRub: '2000.00',
|
||||
deliveredInMonth: 0,
|
||||
requiredLeads: 30,
|
||||
tiers: makeTiers(),
|
||||
);
|
||||
|
||||
expect($result->passes)->toBeTrue();
|
||||
expect($result->capacityLeads)->toBe(40);
|
||||
expect($result->requiredLeads)->toBe(30);
|
||||
expect($result->deficitLeads)->toBe(0);
|
||||
});
|
||||
|
||||
it('fails when balance below full daily limit', function () {
|
||||
$service = new BalancePreflightService;
|
||||
// 1000₽ / 50₽ = 20 лидов capacity; нужно 30 → не проходит, дефицит 10.
|
||||
$result = $service->evaluate(
|
||||
balanceRub: '1000.00',
|
||||
deliveredInMonth: 0,
|
||||
requiredLeads: 30,
|
||||
tiers: makeTiers(),
|
||||
);
|
||||
|
||||
expect($result->passes)->toBeFalse();
|
||||
expect($result->capacityLeads)->toBe(20);
|
||||
expect($result->deficitLeads)->toBe(10);
|
||||
});
|
||||
|
||||
it('passes trivially when requiredLeads is zero', function () {
|
||||
$service = new BalancePreflightService;
|
||||
$result = $service->evaluate('0.00', 0, 0, makeTiers());
|
||||
expect($result->passes)->toBeTrue();
|
||||
});
|
||||
Reference in New Issue
Block a user