65c5178c29
Code-quality review fixups: runway_days клампится в 0 при отрицательном балансе (overdrawn-тенант не должен показывать «−N дней»); (int)-каст в wallet() для консистентности; усилены assertJsonPath на type-фильтре. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
182 lines
6.9 KiB
PHP
182 lines
6.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\BalanceTransaction;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::factory()->create([
|
|
'balance_rub' => '14250.00',
|
|
'balance_leads' => 285,
|
|
]);
|
|
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
|
$this->actingAs($this->user);
|
|
});
|
|
|
|
// ---- wallet ----
|
|
|
|
test('GET /api/billing/wallet возвращает баланс тенанта', function () {
|
|
$this->getJson('/api/billing/wallet')
|
|
->assertOk()
|
|
->assertJsonPath('balance_rub', '14250.00')
|
|
->assertJsonPath('balance_leads', 285);
|
|
});
|
|
|
|
test('GET /api/billing/wallet возвращает тариф, если он назначен', function () {
|
|
$tariffId = DB::table('tariff_plans')->where('code', 'pro')->value('id');
|
|
$this->tenant->update(['current_tariff_id' => $tariffId]);
|
|
|
|
$response = $this->getJson('/api/billing/wallet');
|
|
|
|
$response->assertOk()
|
|
->assertJsonPath('tariff.code', 'pro')
|
|
->assertJsonPath('tariff.name', 'Про');
|
|
expect($response->json('tariff.features'))->toBeArray();
|
|
});
|
|
|
|
test('GET /api/billing/wallet возвращает tariff=null без назначенного тарифа', function () {
|
|
$this->getJson('/api/billing/wallet')
|
|
->assertOk()
|
|
->assertJsonPath('tariff', null);
|
|
});
|
|
|
|
test('GET /api/billing/wallet: runway_days = null без списаний', function () {
|
|
$this->getJson('/api/billing/wallet')
|
|
->assertOk()
|
|
->assertJsonPath('runway_days', null);
|
|
});
|
|
|
|
test('GET /api/billing/wallet: runway_days рассчитан при наличии списаний', function () {
|
|
BalanceTransaction::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'type' => 'lead_charge',
|
|
'amount_rub' => '-3000.00',
|
|
'created_at' => now()->subDays(10),
|
|
]);
|
|
|
|
// 3000 ₽ / 30 дн = 100 ₽/день; баланс 14250 → floor(142.5) = 142.
|
|
expect($this->getJson('/api/billing/wallet')->json('runway_days'))->toBe(142);
|
|
});
|
|
|
|
test('GET /api/billing/wallet: runway_days = 0 при отрицательном балансе', function () {
|
|
$this->tenant->update(['balance_rub' => '-500.00']);
|
|
BalanceTransaction::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'type' => 'lead_charge',
|
|
'amount_rub' => '-3000.00',
|
|
'created_at' => now()->subDays(10),
|
|
]);
|
|
|
|
// Баланс уже отрицательный → runway не может быть отрицательным, клампится в 0.
|
|
expect($this->getJson('/api/billing/wallet')->json('runway_days'))->toBe(0);
|
|
});
|
|
|
|
test('GET /api/billing/wallet без auth: 401', function () {
|
|
auth()->logout();
|
|
$this->getJson('/api/billing/wallet')->assertStatus(401);
|
|
});
|
|
|
|
// ---- transactions ----
|
|
|
|
test('GET /api/billing/transactions возвращает транзакции тенанта', function () {
|
|
BalanceTransaction::factory()->count(3)->create(['tenant_id' => $this->tenant->id]);
|
|
|
|
$response = $this->getJson('/api/billing/transactions');
|
|
|
|
$response->assertOk();
|
|
expect($response->json('data'))->toHaveCount(3);
|
|
expect($response->json('meta.total'))->toBe(3);
|
|
expect($response->json('data.0'))->toHaveKeys(['id', 'code', 'type', 'amount_rub', 'created_at']);
|
|
});
|
|
|
|
test('GET /api/billing/transactions изолирован по тенанту', function () {
|
|
BalanceTransaction::factory()->create(['tenant_id' => $this->tenant->id]);
|
|
$other = Tenant::factory()->create();
|
|
BalanceTransaction::factory()->create(['tenant_id' => $other->id]);
|
|
|
|
expect($this->getJson('/api/billing/transactions')->json('data'))->toHaveCount(1);
|
|
});
|
|
|
|
test('GET /api/billing/transactions фильтрует по type', function () {
|
|
BalanceTransaction::factory()->create(['tenant_id' => $this->tenant->id, 'type' => 'topup']);
|
|
BalanceTransaction::factory()->create(['tenant_id' => $this->tenant->id, 'type' => 'lead_charge', 'amount_rub' => '-50.00']);
|
|
BalanceTransaction::factory()->create(['tenant_id' => $this->tenant->id, 'type' => 'refund', 'amount_rub' => '10.00']);
|
|
|
|
$this->getJson('/api/billing/transactions?type=topup')
|
|
->assertJsonCount(1, 'data')->assertJsonPath('data.0.type', 'topup');
|
|
$this->getJson('/api/billing/transactions?type=lead_charge')
|
|
->assertJsonCount(1, 'data')->assertJsonPath('data.0.type', 'lead_charge');
|
|
$this->getJson('/api/billing/transactions?type=refund')
|
|
->assertJsonCount(1, 'data')->assertJsonPath('data.0.type', 'refund');
|
|
});
|
|
|
|
test('GET /api/billing/transactions: пагинация 20/страница', function () {
|
|
BalanceTransaction::factory()->count(25)->create(['tenant_id' => $this->tenant->id]);
|
|
|
|
expect($this->getJson('/api/billing/transactions?page=1')->json('data'))->toHaveCount(20);
|
|
expect($this->getJson('/api/billing/transactions?page=2')->json('data'))->toHaveCount(5);
|
|
});
|
|
|
|
test('GET /api/billing/transactions без auth: 401', function () {
|
|
auth()->logout();
|
|
$this->getJson('/api/billing/transactions')->assertStatus(401);
|
|
});
|
|
|
|
// ---- invoices ----
|
|
|
|
test('GET /api/billing/invoices возвращает пустой список без счетов', function () {
|
|
$this->getJson('/api/billing/invoices')
|
|
->assertOk()
|
|
->assertJsonCount(0, 'data');
|
|
});
|
|
|
|
test('GET /api/billing/invoices возвращает счета тенанта и изолирует чужие', function () {
|
|
$leId = DB::table('legal_entities')->insertGetId([
|
|
'code' => 'ooo_test_'.uniqid(),
|
|
'name' => 'ООО Тест',
|
|
'legal_form' => 'OOO',
|
|
'inn' => '7700000000',
|
|
'created_at' => now(),
|
|
]);
|
|
DB::table('saas_invoices')->insert([
|
|
'tenant_id' => $this->tenant->id,
|
|
'legal_entity_id' => $leId,
|
|
'invoice_number' => 'СЧ-2026-00001',
|
|
'payer_type' => 'legal',
|
|
'amount_net' => '990.00',
|
|
'amount_total' => '990.00',
|
|
'status' => 'issued',
|
|
'issued_at' => now(),
|
|
'expires_at' => now()->addDays(5),
|
|
]);
|
|
$other = Tenant::factory()->create();
|
|
DB::table('saas_invoices')->insert([
|
|
'tenant_id' => $other->id,
|
|
'legal_entity_id' => $leId,
|
|
'invoice_number' => 'СЧ-2026-00002',
|
|
'payer_type' => 'legal',
|
|
'amount_net' => '500.00',
|
|
'amount_total' => '500.00',
|
|
'status' => 'issued',
|
|
'issued_at' => now(),
|
|
'expires_at' => now()->addDays(5),
|
|
]);
|
|
|
|
$response = $this->getJson('/api/billing/invoices');
|
|
|
|
$response->assertOk();
|
|
expect($response->json('data'))->toHaveCount(1);
|
|
expect($response->json('data.0.invoice_number'))->toBe('СЧ-2026-00001');
|
|
});
|
|
|
|
test('GET /api/billing/invoices без auth: 401', function () {
|
|
auth()->logout();
|
|
$this->getJson('/api/billing/invoices')->assertStatus(401);
|
|
});
|