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