e1601e7862
Spec C §3.6/§6.2. Бэкенд: GET /api/billing/balance-status (frozen + capacity + required + дефицит ₽/leads), Pest 6. Фронт: BalanceFrozenBanner (в AppLayout, глобально), BalanceCapacityIndicator (в BillingView под балансом), ProjectLimitOverloadDialog (409-перехват в NewProjectDialog: save-blocked/set-zero), tenantStore + api getBalanceStatus. Vitest +18. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
108 lines
4.0 KiB
PHP
108 lines
4.0 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Models\Project;
|
||
use App\Models\Tenant;
|
||
use App\Models\User;
|
||
use Database\Seeders\PricingTierSeeder;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
|
||
uses(DatabaseTransactions::class);
|
||
|
||
/**
|
||
* GET /api/billing/balance-status — статус баланса для UI префлайта (Billing v2
|
||
* Spec C Task 1.10): питает баннер заморозки (BalanceFrozenBanner) и индикатор
|
||
* ёмкости (BalanceCapacityIndicator).
|
||
*
|
||
* PricingTierSeeder: ступень 1 — 100 лидов × 500₽ (см. BillingOverviewControllerTest).
|
||
*/
|
||
beforeEach(function () {
|
||
$this->seed(PricingTierSeeder::class);
|
||
$this->tenant = Tenant::factory()->create([
|
||
'balance_rub' => '5000.00',
|
||
'delivered_in_month' => 0,
|
||
'frozen_by_balance_at' => null,
|
||
]);
|
||
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
||
$this->actingAs($this->user);
|
||
});
|
||
|
||
test('GET /api/billing/balance-status: структура ответа', function () {
|
||
$this->getJson('/api/billing/balance-status')
|
||
->assertOk()
|
||
->assertJsonStructure([
|
||
'frozen_by_balance_at',
|
||
'balance_rub',
|
||
'capacity_leads',
|
||
'required_leads_per_day',
|
||
'deficit_leads',
|
||
'deficit_rub',
|
||
]);
|
||
});
|
||
|
||
test('balance-status: хватает баланса — deficit=0, не заморожен', function () {
|
||
// 5000₽ при ступени 1 (500₽/лид) = 10 лидов ёмкости. Проект лимит 5 — впритык.
|
||
Project::factory()->create([
|
||
'tenant_id' => $this->tenant->id,
|
||
'is_active' => true,
|
||
'daily_limit_target' => 5,
|
||
]);
|
||
|
||
$resp = $this->getJson('/api/billing/balance-status')->assertOk();
|
||
|
||
expect($resp->json('balance_rub'))->toBe('5000.00');
|
||
expect($resp->json('capacity_leads'))->toBe(10);
|
||
expect($resp->json('required_leads_per_day'))->toBe(5);
|
||
expect($resp->json('deficit_leads'))->toBe(0);
|
||
expect($resp->json('deficit_rub'))->toBe('0.00');
|
||
expect($resp->json('frozen_by_balance_at'))->toBeNull();
|
||
});
|
||
|
||
test('balance-status: не хватает — deficit_leads + deficit_rub точные', function () {
|
||
// Ёмкость = 10 лидов. Проект лимит 25 → нужно 25, дефицит 15 лидов.
|
||
// minBalanceForLeads(25) = 25 × 500₽ = 12500₽ → deficit_rub = 12500 − 5000 = 7500.00.
|
||
Project::factory()->create([
|
||
'tenant_id' => $this->tenant->id,
|
||
'is_active' => true,
|
||
'daily_limit_target' => 25,
|
||
]);
|
||
|
||
$resp = $this->getJson('/api/billing/balance-status')->assertOk();
|
||
|
||
expect($resp->json('capacity_leads'))->toBe(10);
|
||
expect($resp->json('required_leads_per_day'))->toBe(25);
|
||
expect($resp->json('deficit_leads'))->toBe(15);
|
||
expect($resp->json('deficit_rub'))->toBe('7500.00');
|
||
});
|
||
|
||
test('balance-status: required исключает inactive и preflight_blocked проекты', function () {
|
||
Project::factory()->create([
|
||
'tenant_id' => $this->tenant->id, 'is_active' => true, 'daily_limit_target' => 5,
|
||
]);
|
||
Project::factory()->create([
|
||
'tenant_id' => $this->tenant->id, 'is_active' => false, 'daily_limit_target' => 100,
|
||
]);
|
||
Project::factory()->create([
|
||
'tenant_id' => $this->tenant->id, 'is_active' => true, 'daily_limit_target' => 100,
|
||
'preflight_blocked_at' => now(),
|
||
]);
|
||
|
||
$resp = $this->getJson('/api/billing/balance-status')->assertOk();
|
||
|
||
expect($resp->json('required_leads_per_day'))->toBe(5);
|
||
});
|
||
|
||
test('balance-status: возвращает frozen_by_balance_at когда установлен', function () {
|
||
$this->tenant->update(['frozen_by_balance_at' => now()]);
|
||
|
||
$resp = $this->getJson('/api/billing/balance-status')->assertOk();
|
||
|
||
expect($resp->json('frozen_by_balance_at'))->not->toBeNull();
|
||
});
|
||
|
||
test('GET /api/billing/balance-status без auth: 401', function () {
|
||
auth()->logout();
|
||
$this->getJson('/api/billing/balance-status')->assertStatus(401);
|
||
});
|