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

127 lines
6.1 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Jobs\Billing\BalanceFrozenReminderJob;
use App\Jobs\Billing\BalancePreflightSweepJob;
use App\Jobs\SyncSupplierProjectJob;
use App\Mail\BalanceFrozenReminderMail;
use App\Models\PricingTier;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Billing\BillingTopupService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
// Косяк 01 (24.06.2026): при нескольких версиях тарифа (старая дорогая + новая дешёвая,
// обе is_active — это нормальное версионирование цены по effective_from) расчёт баланса
// и снятия блока ДОЛЖЕН брать ДЕЙСТВУЮЩУЮ версию по дате — как списание и витрина через
// PricingTierRepository::activeAt — а не «по-простому» PricingTier::where(is_active)->get(),
// которое садится на старую дорогую версию (садится на запись с меньшим id).
beforeEach(function () {
Queue::fake();
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
// СТАРАЯ дорогая версия — создаём ПЕРВОЙ (ниже id → наивный sortBy('tier_no') садится на неё).
PricingTier::query()->create([
'tier_no' => 1,
'leads_in_tier' => 100,
'price_per_lead_kopecks' => 50000, // 500₽/лид — устаревшая версия
'is_active' => true,
'effective_from' => '2020-01-01',
]);
// НОВАЯ действующая версия — свежий effective_from.
PricingTier::query()->create([
'tier_no' => 1,
'leads_in_tier' => 100,
'price_per_lead_kopecks' => 5000, // 50₽/лид — действующая версия
'is_active' => true,
'effective_from' => now(),
]);
});
it('topup unblocks project using the CURRENT cheap tariff, not the stale expensive one', function () {
$tenant = Tenant::factory()->withRequisites()->create(['balance_rub' => '0.00']);
$project = Project::factory()->for($tenant)->create([
'is_active' => true,
'daily_limit_target' => 30,
'preflight_blocked_at' => now(),
]);
// 2000₽: по действующей 50₽/лид = 40 лидов >= 30 → блок снять.
// (по устаревшей 500₽/лид = 4 лида < 30 → остался бы заблокирован — это и есть баг.)
app(BillingTopupService::class)->topup($tenant->id, '2000.00', null);
expect($project->fresh()->preflight_blocked_at)->toBeNull();
Queue::assertPushed(SyncSupplierProjectJob::class);
});
it('creates project (no 409) when limit is affordable at the CURRENT tariff', function () {
// 2000₽: действующая 50₽ = 40 лидов >= 30 → создать (по устаревшей 500₽ = 4 < 30 → 409 = БАГ).
$tenant = Tenant::factory()->withRequisites()->create(['balance_rub' => '2000.00']);
$user = User::factory()->create(['tenant_id' => $tenant->id]);
$this->actingAs($user)->postJson('/api/projects', [
'name' => 'По действующей цене',
'signal_type' => 'site',
'signal_identifier' => 'current-tariff.ru',
'daily_limit_target' => 30,
'regions' => [],
'delivery_days_mask' => 127,
])->assertStatus(201);
expect(Project::where('signal_identifier', 'current-tariff.ru')->exists())->toBeTrue();
});
it('applies bulk limit increase affordable at the CURRENT tariff', function () {
// 2000₽: действующая 50₽ = 40 лидов. Два проекта по 15 → сумма 30 ≤ 40 → применить.
// (по устаревшей 500₽ = 4 < 30 → оба снялись бы = БАГ.)
$tenant = Tenant::factory()->withRequisites()->create(['balance_rub' => '2000.00']);
$user = User::factory()->create(['tenant_id' => $tenant->id]);
$p1 = Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 5, 'delivered_today' => 0]);
$p2 = Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 5, 'delivered_today' => 0]);
$this->actingAs($user)->postJson('/api/projects/bulk', [
'action' => 'update_limit',
'ids' => [$p1->id, $p2->id],
'replace' => 15,
])->assertOk()->assertJsonPath('updated', 2);
expect($p1->fresh()->daily_limit_target)->toBe(15);
expect($p2->fresh()->daily_limit_target)->toBe(15);
});
it('does NOT freeze tenant affordable at the CURRENT tariff during the sweep', function () {
// 2000₽: действующая 50₽ = 40 лидов >= 30 → НЕ замораживать.
// (по устаревшей 500₽ = 4 < 30 → заморозил бы = БАГ.)
$tenant = Tenant::factory()->create(['balance_rub' => '2000.00', 'frozen_by_balance_at' => null]);
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 30]);
(new BalancePreflightSweepJob)->handle();
expect($tenant->fresh()->frozen_by_balance_at)->toBeNull();
});
it('reminder email shows capacity at the CURRENT tariff, not the stale one', function () {
Mail::fake();
// Замороженный клиент в окне reminder (25ч). 2000₽, лимит 100 → нужно 100.
// действующая 50₽ = 40 лидов вместимость; устаревшая 500₽ = 4 (это попало бы в письмо = БАГ).
$tenant = Tenant::factory()->create([
'balance_rub' => '2000.00',
'frozen_by_balance_at' => now()->subHours(25),
]);
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 100]);
(new BalanceFrozenReminderJob)->handle();
Mail::assertQueued(BalanceFrozenReminderMail::class, fn ($mail) => $mail->result->capacityLeads === 40);
});