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