89 lines
3.3 KiB
PHP
89 lines
3.3 KiB
PHP
<?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);
|
||
});
|