2026-05-11 09:05:22 +03:00
|
|
|
<?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');
|
|
|
|
|
});
|
2026-05-11 09:17:13 +03:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|