1e0c0ab90a
- 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>
70 lines
2.7 KiB
PHP
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');
|
|
});
|