Files
portal/app/tests/Feature/Billing/ReleaseArmedProjectsTest.php
T

128 lines
5.9 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Jobs\SyncSupplierProjectJob;
use App\Models\PricingTier;
use App\Models\Project;
use App\Models\Tenant;
use App\Services\Billing\ProjectBlockReleaseService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
beforeEach(function () {
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
PricingTier::query()->create(['tier_no' => 1, 'leads_in_tier' => null, 'price_per_lead_kopecks' => 5000, 'is_active' => true, 'effective_from' => now()]);
});
it('activates an armed-off project on topup when balance covers it', function () {
Queue::fake();
// 1000₽ / 50₽ = 20 лидов ёмкость; взведённый проект хочет 5 → хватает.
$tenant = Tenant::factory()->create(['balance_rub' => '1000.00', 'frozen_by_balance_at' => null]);
$armed = Project::factory()->for($tenant)->create([
'is_active' => false,
'preflight_blocked_at' => now(),
'paused_at' => now(),
'daily_limit_target' => 5,
]);
(new ProjectBlockReleaseService)->releaseForTenant($tenant->id);
$fresh = $armed->fresh();
expect($fresh->is_active)->toBeTrue();
expect($fresh->preflight_blocked_at)->toBeNull();
expect($fresh->paused_at)->toBeNull();
Queue::assertPushed(SyncSupplierProjectJob::class, fn (SyncSupplierProjectJob $j) => $j->projectId === $armed->id);
});
it('activates ALL armed-off when balance covers all', function () {
Queue::fake();
// 1000₽/50 = 20 лидов; два проекта по 5 = 10 → хватает на оба.
$tenant = Tenant::factory()->create(['balance_rub' => '1000.00']);
$a = Project::factory()->for($tenant)->create(['is_active' => false, 'preflight_blocked_at' => now(), 'paused_at' => now(), 'daily_limit_target' => 5]);
$b = Project::factory()->for($tenant)->create(['is_active' => false, 'preflight_blocked_at' => now(), 'paused_at' => now(), 'daily_limit_target' => 5]);
(new ProjectBlockReleaseService)->releaseForTenant($tenant->id);
expect($a->fresh()->is_active)->toBeTrue();
expect($b->fresh()->is_active)->toBeTrue();
});
it('activates NONE when balance covers only some (all-or-nothing)', function () {
Queue::fake();
// 500₽/50 = 10 лидов; два проекта по 8 = 16 → не хватает на оба → ни одного.
$tenant = Tenant::factory()->create(['balance_rub' => '500.00']);
$a = Project::factory()->for($tenant)->create(['is_active' => false, 'preflight_blocked_at' => now(), 'paused_at' => now(), 'daily_limit_target' => 8]);
$b = Project::factory()->for($tenant)->create(['is_active' => false, 'preflight_blocked_at' => now(), 'paused_at' => now(), 'daily_limit_target' => 8]);
(new ProjectBlockReleaseService)->releaseForTenant($tenant->id);
expect($a->fresh()->is_active)->toBeFalse();
expect($a->fresh()->preflight_blocked_at)->not->toBeNull();
expect($b->fresh()->is_active)->toBeFalse();
});
it('counts armed leads in the gate (active fits alone but not active+armed)', function () {
Queue::fake();
// 600₽/50 = 12 лидов. Активный хочет 10 (влезает один). Взведённый ещё 5 → 15 > 12 → не включаем.
$tenant = Tenant::factory()->create(['balance_rub' => '600.00']);
Project::factory()->for($tenant)->create(['is_active' => true, 'preflight_blocked_at' => null, 'daily_limit_target' => 10]);
$armed = Project::factory()->for($tenant)->create(['is_active' => false, 'preflight_blocked_at' => now(), 'paused_at' => now(), 'daily_limit_target' => 5]);
(new ProjectBlockReleaseService)->releaseForTenant($tenant->id);
expect($armed->fresh()->is_active)->toBeFalse();
});
it('does NOT touch a draft (no balance mark)', function () {
Queue::fake();
$tenant = Tenant::factory()->create(['balance_rub' => '5000.00']);
$draft = Project::factory()->for($tenant)->create(['is_active' => false, 'preflight_blocked_at' => null, 'paused_at' => null, 'daily_limit_target' => 5]);
(new ProjectBlockReleaseService)->releaseForTenant($tenant->id);
expect($draft->fresh()->is_active)->toBeFalse();
});
it('does NOT touch a client-paused project (no balance mark)', function () {
Queue::fake();
$tenant = Tenant::factory()->create(['balance_rub' => '5000.00']);
$paused = Project::factory()->for($tenant)->create(['is_active' => false, 'preflight_blocked_at' => null, 'paused_at' => now(), 'daily_limit_target' => 5]);
(new ProjectBlockReleaseService)->releaseForTenant($tenant->id);
expect($paused->fresh()->is_active)->toBeFalse();
expect($paused->fresh()->paused_at)->not->toBeNull();
});
it('still clears block on an armed-ON project (regression)', function () {
Queue::fake();
$tenant = Tenant::factory()->create(['balance_rub' => '5000.00']);
$armedOn = Project::factory()->for($tenant)->create(['is_active' => true, 'preflight_blocked_at' => now(), 'daily_limit_target' => 5]);
(new ProjectBlockReleaseService)->releaseForTenant($tenant->id);
expect($armedOn->fresh()->is_active)->toBeTrue();
expect($armedOn->fresh()->preflight_blocked_at)->toBeNull();
});
it('is idempotent — second run at same balance changes nothing', function () {
Queue::fake();
$tenant = Tenant::factory()->create(['balance_rub' => '1000.00']);
$armed = Project::factory()->for($tenant)->create(['is_active' => false, 'preflight_blocked_at' => now(), 'paused_at' => now(), 'daily_limit_target' => 5]);
$svc = new ProjectBlockReleaseService;
$svc->releaseForTenant($tenant->id);
$afterFirst = $armed->fresh()->is_active;
$svc->releaseForTenant($tenant->id);
expect($afterFirst)->toBeTrue();
expect($armed->fresh()->is_active)->toBeTrue();
expect($armed->fresh()->preflight_blocked_at)->toBeNull();
});