Files
portal/app/tests/Unit/Services/Billing/BalanceToLeadsConverterTest.php
T

89 lines
3.3 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\PricingTier;
use App\Services\Billing\BalanceToLeadsConverter;
use Illuminate\Database\Eloquent\Collection;
function buildTier(int $no, ?int $cap, int $priceKopecks): PricingTier
{
$t = new PricingTier;
$t->tier_no = $no;
$t->leads_in_tier = $cap;
$t->price_per_lead_kopecks = $priceKopecks;
$t->is_active = true;
return $t;
}
beforeEach(function () {
$this->tiers = new Collection([
buildTier(1, 50, 12000),
buildTier(2, 100, 10000),
buildTier(3, 200, 8000),
buildTier(7, null, 6000),
]);
});
it('returns 0 leads when balance is zero', function () {
$result = (new BalanceToLeadsConverter)->convert('0.00', 0, $this->tiers);
expect($result['leads'])->toBe(0);
});
it('takes only tier 1 when balance covers only first tier', function () {
$result = (new BalanceToLeadsConverter)->convert('1200.00', 0, $this->tiers);
expect($result['leads'])->toBe(10);
expect($result['breakdown'])->toHaveCount(1);
expect($result['breakdown'][0]['tier_no'])->toBe(1);
expect($result['breakdown'][0]['leads'])->toBe(10);
});
it('crosses tier 1 → tier 2 with deliveredInMonth=30', function () {
// 30 already delivered, 5000 ₽ remaining
// tier 1 has 20 slots × 120₽ = 2400 ₽ → 20 leads, balance = 2600 ₽
// tier 2 has 100 slots, 2600/100 = 26 leads → 26 leads
// total = 46
$result = (new BalanceToLeadsConverter)->convert('5000.00', 30, $this->tiers);
expect($result['leads'])->toBe(46);
expect($result['breakdown'])->toHaveCount(2);
expect($result['breakdown'][0])->toMatchArray(['tier_no' => 1, 'leads' => 20]);
expect($result['breakdown'][1])->toMatchArray(['tier_no' => 2, 'leads' => 26]);
expect($result['current_tier']['no'])->toBe(1);
expect($result['current_tier']['leads_left_in_tier'])->toBe(20);
expect($result['next_tier']['no'])->toBe(2);
});
it('skips already-exhausted tier 1 when deliveredInMonth=50', function () {
$result = (new BalanceToLeadsConverter)->convert('1000.00', 50, $this->tiers);
// tier 1 exhausted; on tier 2: 1000/100 = 10 leads
expect($result['leads'])->toBe(10);
expect($result['current_tier']['no'])->toBe(2);
});
it('uses last tier with leads_in_tier=NULL as catch-all', function () {
$result = (new BalanceToLeadsConverter)->convert('600.00', 350, $this->tiers);
// tier 1+2+3 exhausted (350 = 50+100+200). Tier 7: 600/60 = 10 leads.
expect($result['leads'])->toBe(10);
expect($result['current_tier']['no'])->toBe(7);
});
it('handles exact-kopeck balance precisely (no float drift)', function () {
$result = (new BalanceToLeadsConverter)->convert('120.00', 0, $this->tiers);
expect($result['leads'])->toBe(1);
});
it('returns 0 when balance is less than tier 1 price', function () {
$result = (new BalanceToLeadsConverter)->convert('119.99', 0, $this->tiers);
expect($result['leads'])->toBe(0);
});
it('ignores inactive tiers', function () {
$inactive = buildTier(2, 100, 1);
$inactive->is_active = false;
$tiers = new Collection([buildTier(1, 50, 12000), $inactive, buildTier(7, null, 6000)]);
$result = (new BalanceToLeadsConverter)->convert('5000.00', 50, $tiers);
// tier 1 exhausted, tier 2 inactive (skipped), tier 7: 5000/60 = 83 leads
expect($result['leads'])->toBe(83);
});