Files
portal/app/tests/Unit/Billing/PricingTierResolverTest.php
T
Дмитрий 1e0c0ab90a fix(billing): Plan 4 Task 2 code-review fixes (2 Important + 1 Minor)
- PricingTierResolver::resolveForCount — InvalidArgumentException на
  $leadOrdinal < 1 (closes I-1: defensive contract validation).
- PricingTierRepository::activeAt — explicit @var Collection<int,
  PricingTier> annotation для type narrowing (closes I-2; firstOrFail
  отвергнут — Stan ругается на Eloquent\Model return-type).
- PricingTierResolverTest — +1 unit test (8/8 PASS): throws на 0/-1.
- PricingTierRepositoryTest — +1 integration test (5/5 PASS): excludes
  inactive tiers (closes M-2 coverage gap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 09:17:13 +03:00

70 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\PricingTier;
use App\Services\Billing\PricingTierResolver;
use Illuminate\Database\Eloquent\Collection;
use Tests\TestCase;
uses(TestCase::class);
beforeEach(function () {
$this->resolver = new PricingTierResolver;
// 7-tier сетка in-memory (без БД)
$this->tiers = new Collection([
new PricingTier(['tier_no' => 1, 'leads_in_tier' => 100, 'price_per_lead_kopecks' => 50000]),
new PricingTier(['tier_no' => 2, 'leads_in_tier' => 200, 'price_per_lead_kopecks' => 45000]),
new PricingTier(['tier_no' => 3, 'leads_in_tier' => 400, 'price_per_lead_kopecks' => 40000]),
new PricingTier(['tier_no' => 4, 'leads_in_tier' => 800, 'price_per_lead_kopecks' => 35000]),
new PricingTier(['tier_no' => 5, 'leads_in_tier' => 1500, 'price_per_lead_kopecks' => 30000]),
new PricingTier(['tier_no' => 6, 'leads_in_tier' => 3000, 'price_per_lead_kopecks' => 27000]),
new PricingTier(['tier_no' => 7, 'leads_in_tier' => null, 'price_per_lead_kopecks' => 25000]),
]);
});
it('returns tier 1 for the 1st lead', function () {
$tier = $this->resolver->resolveForCount($this->tiers, 1);
expect($tier->tier_no)->toBe(1);
});
it('returns tier 1 at upper bound (100th lead)', function () {
$tier = $this->resolver->resolveForCount($this->tiers, 100);
expect($tier->tier_no)->toBe(1);
});
it('crosses to tier 2 at 101st lead', function () {
$tier = $this->resolver->resolveForCount($this->tiers, 101);
expect($tier->tier_no)->toBe(2);
});
it('returns tier 6 at cumulative sum of tiers 1-6 (6000th lead)', function () {
// 100 + 200 + 400 + 800 + 1500 + 3000 = 6000; last in tier 6 = 6000th lead
$tier = $this->resolver->resolveForCount($this->tiers, 6000);
expect($tier->tier_no)->toBe(6);
});
it('returns tier 7 (unlimited) for 6001st lead', function () {
$tier = $this->resolver->resolveForCount($this->tiers, 6001);
expect($tier->tier_no)->toBe(7);
});
it('returns tier 7 for 1_000_000th lead (unlimited)', function () {
$tier = $this->resolver->resolveForCount($this->tiers, 1_000_000);
expect($tier->tier_no)->toBe(7);
});
it('throws RuntimeException on empty tiers collection', function () {
expect(fn () => $this->resolver->resolveForCount(new Collection, 1))
->toThrow(RuntimeException::class, 'No active pricing tiers');
});
it('throws InvalidArgumentException on zero or negative ordinal', function () {
expect(fn () => $this->resolver->resolveForCount($this->tiers, 0))
->toThrow(InvalidArgumentException::class, 'leadOrdinal must be >= 1');
expect(fn () => $this->resolver->resolveForCount($this->tiers, -5))
->toThrow(InvalidArgumentException::class, 'leadOrdinal must be >= 1');
});