128 lines
5.9 KiB
PHP
128 lines
5.9 KiB
PHP
|
|
<?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();
|
||
|
|
});
|