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); });