user()->tenant_id` фильтр — другие tenant'ы видны только если * мы сами их INSERT'нули, но фильтрация теста на $this->tenant->id всё равно * остаётся защитой. * * Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §6.3 */ uses(DatabaseTransactions::class); beforeEach(function () { // PricingTierSeeder идемпотентен (updateOrCreate); seed безопасно. $this->seed(PricingTierSeeder::class); $this->tenant = Tenant::factory()->create(); $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]); $this->actingAs($this->user); }); function makeChargeFor(Tenant $tenant, array $overrides = []): LeadCharge { $deal = Deal::factory()->create([ 'tenant_id' => $tenant->id, 'received_at' => now(), ]); return LeadCharge::factory()->create(array_merge([ 'tenant_id' => $tenant->id, 'deal_id' => $deal->id, 'deal_received_at' => $deal->received_at, 'charged_at' => now(), ], $overrides)); } it('GET /api/billing/charges returns paginated list for current tenant only (RLS)', function () { makeChargeFor($this->tenant); makeChargeFor($this->tenant); $otherTenant = Tenant::factory()->create(); makeChargeFor($otherTenant); $response = $this->getJson('/api/billing/charges'); $response->assertOk(); expect($response->json('data'))->toHaveCount(2); }); it('filters by charge_source=prepaid', function () { makeChargeFor($this->tenant, ['charge_source' => 'rub', 'price_per_lead_kopecks' => 50000]); makeChargeFor($this->tenant, ['charge_source' => 'prepaid', 'price_per_lead_kopecks' => 0]); makeChargeFor($this->tenant, ['charge_source' => 'prepaid', 'price_per_lead_kopecks' => 0]); $response = $this->getJson('/api/billing/charges?charge_source=prepaid'); expect($response->json('data'))->toHaveCount(2); }); it('filters by period=current_month / last_month / 90d', function () { makeChargeFor($this->tenant, ['charged_at' => now()]); makeChargeFor($this->tenant, ['charged_at' => now()->subMonth()]); makeChargeFor($this->tenant, ['charged_at' => now()->subDays(60)]); makeChargeFor($this->tenant, ['charged_at' => now()->subDays(120)]); $this->getJson('/api/billing/charges?period=current_month')->assertJsonCount(1, 'data'); $this->getJson('/api/billing/charges?period=last_month')->assertJsonCount(1, 'data'); $this->getJson('/api/billing/charges?period=90d')->assertJsonCount(3, 'data'); }); it('returns 401 без auth', function () { auth()->logout(); $this->getJson('/api/billing/charges')->assertStatus(401); }); it('pagination: ?page=2 returns next slice', function () { for ($i = 0; $i < 30; $i++) { makeChargeFor($this->tenant); } $page1 = $this->getJson('/api/billing/charges?page=1'); $page2 = $this->getJson('/api/billing/charges?page=2'); expect($page1->json('data'))->toHaveCount(20); expect($page2->json('data'))->toHaveCount(10); }); it('POST /export streams CSV via StreamedResponse', function () { makeChargeFor($this->tenant); $response = $this->postJson('/api/billing/charges/export', ['period' => '90d']); $response->assertOk(); $response->assertHeader('Content-Type', 'text/csv; charset=UTF-8'); $body = $response->streamedContent(); expect($body)->toContain('charged_at,deal_id,tier_no,charge_source,price_rub,balance_rub_after'); });