actingAs($user, 'sales'). * Изоляция: DatabaseTransactions. Глобально подключён SharesAdminPdo (Pest.php). * DB_DATABASE=liderra_testing ОБЯЗАТЕЛЕН при запуске. */ uses(DatabaseTransactions::class); // ── helpers (уникальный префикс sinv_) ─────────────────────────────────────── function sinv_head(): SalesUser { return SalesUser::create([ 'name' => 'Начальник '.uniqid(), 'email' => 'sinvhead'.uniqid().'@sales.local', 'password' => bcrypt('secret'), 'role' => 'head', 'is_active' => true, ]); } function sinv_manager(): SalesUser { return SalesUser::create([ 'name' => 'Менеджер '.uniqid(), 'email' => 'sinvmgr'.uniqid().'@sales.local', 'password' => bcrypt('secret'), 'role' => 'manager', 'is_active' => true, ]); } /** * Счёт issued + тенант + получатель письма (зеркалит seedPaidScenario из * InvoiceMarkPaidTest — тот же рецепт tenant/legal_entity/saas_invoice). * * @return array{0: Tenant, 1: SaasInvoice} */ function sinv_scenario(string $balance = '100.00', string $amount = '1500.00', string $status = SaasInvoice::STATUS_ISSUED): array { $tenant = Tenant::factory()->create(['balance_rub' => $balance]); User::factory()->create(['tenant_id' => $tenant->id]); // Единственный default LE на тест: unique-индекс idx_legal_entities_default // допускает лишь одну строку с is_default=true, поэтому переиспользуем. $le = LegalEntity::query()->where('is_default', true)->first() ?? LegalEntity::create([ 'code' => 'mp_'.uniqid(), 'name' => 'ИП Лидерра', 'legal_form' => 'IP', 'inn' => '770000000099', 'is_default' => true, ]); $invoice = SaasInvoice::create([ 'tenant_id' => $tenant->id, 'legal_entity_id' => $le->id, 'invoice_number' => 'СЧ-2026-'.substr(uniqid(), -6), 'payer_type' => 'legal', 'payer_name' => 'ООО Плательщик', 'payer_inn' => '5000000000', 'amount_net' => $amount, 'amount_total' => $amount, 'status' => $status, 'issued_at' => now(), 'expires_at' => now()->addDays(5), ]); return [$tenant, $invoice]; } // ── 1. доступ: менеджер (не head) → 403 ────────────────────────────────────── test('менеджер GET /api/sales/invoices → 403', function () { $manager = sinv_manager(); $this->actingAs($manager, 'sales')->getJson('/api/sales/invoices') ->assertStatus(403) ->assertJson(['message' => 'Доступно только начальнику отдела.']); }); test('менеджер POST /api/sales/invoices/{id}/mark-paid → 403', function () { $manager = sinv_manager(); [, $invoice] = sinv_scenario(); $this->actingAs($manager, 'sales')->postJson("/api/sales/invoices/{$invoice->id}/mark-paid") ->assertStatus(403) ->assertJson(['message' => 'Доступно только начальнику отдела.']); }); test('неаутентифицированный GET /api/sales/invoices → 401', function () { $this->getJson('/api/sales/invoices')->assertUnauthorized(); }); // ── 2. index: список счетов + поля + фильтр status ─────────────────────────── test('head: index отдаёт список счетов с ожидаемыми полями', function () { $head = sinv_head(); [$tenant, $invoice] = sinv_scenario('0.00', '2500.00'); $response = $this->actingAs($head, 'sales')->getJson('/api/sales/invoices'); $response->assertOk(); $row = collect($response->json('data'))->firstWhere('id', $invoice->id); expect($row)->not->toBeNull() ->and($row['invoice_number'])->toBe($invoice->invoice_number) ->and((float) $row['amount_total'])->toBe(2500.0) ->and($row['status'])->toBe(SaasInvoice::STATUS_ISSUED) ->and($row['payer_name'])->toBe('ООО Плательщик') ->and($row)->toHaveKeys([ 'id', 'invoice_number', 'amount_total', 'status', 'tenant_name', 'payer_name', 'issued_at', 'expires_at', ]); expect($response->json('meta'))->toHaveKeys(['current_page', 'last_page', 'total', 'per_page']); }); test('head: index фильтр status=paid отсекает issued-счета', function () { $head = sinv_head(); [, $issued] = sinv_scenario('0.00', '1000.00', SaasInvoice::STATUS_ISSUED); [, $paid] = sinv_scenario('0.00', '2000.00', SaasInvoice::STATUS_PAID); $data = $this->actingAs($head, 'sales') ->getJson('/api/sales/invoices?status=paid')->json('data'); $ids = array_column($data, 'id'); expect($ids)->toContain($paid->id) ->and($ids)->not->toContain($issued->id); }); // ── 3. mark-paid: делегирование зачисления ─────────────────────────────────── test('head: mark-paid по issued-счёту → 200, счёт paid, баланс пополнен', function () { Storage::fake('local'); Mail::fake(); $head = sinv_head(); [$tenant, $invoice] = sinv_scenario('100.00', '1500.00'); $response = $this->actingAs($head, 'sales') ->postJson("/api/sales/invoices/{$invoice->id}/mark-paid"); $response->assertOk()->assertJson(['status' => 'ok']); $this->assertDatabaseHas('saas_invoices', ['id' => $invoice->id, 'status' => 'paid']); $tenant->refresh(); expect((string) $tenant->balance_rub)->toBe('1600.00') ->and(SaasTransaction::where('invoice_id', $invoice->id)->where('status', 'success')->count())->toBe(1) ->and(SaasUpdDocument::where('invoice_id', $invoice->id)->count())->toBe(1); }); test('head: повторный mark-paid идемпотентен — баланс не удваивается', function () { Storage::fake('local'); Mail::fake(); $head = sinv_head(); [$tenant, $invoice] = sinv_scenario('0.00', '500.00'); $this->actingAs($head, 'sales')->postJson("/api/sales/invoices/{$invoice->id}/mark-paid")->assertOk(); $this->actingAs($head, 'sales')->postJson("/api/sales/invoices/{$invoice->id}/mark-paid")->assertOk(); $tenant->refresh(); expect((string) $tenant->balance_rub)->toBe('500.00') ->and(SaasUpdDocument::where('invoice_id', $invoice->id)->count())->toBe(1); });