Files
portal/app/tests/Feature/Billing/AdminInvoiceIndexTest.php
T
Дмитрий cdfae077a3
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
feat(биллинг): оплата по счёту (Этап 1) — счёт, акт, отметка оплаты
Клиент сам выставляет PDF-счёт (TopupDialog вкладка «По счёту»), счета и
акты — в отдельной вкладке «Счета». Админ (/admin/invoices) отмечает оплату
одной кнопкой → атомарно зачисляет баланс (BillingTopupService), формирует
Акт (без НДС, saas_upd_documents ДОП) и шлёт клиенту письмо «Счёт оплачен»
с вложением PDF-акта. PDF открываются inline в браузере (ASCII-имя).

- Сервисы InvoiceNumberGenerator/InvoiceService/ActService/InvoicePaymentService/PdfRenderer
- Контроллеры InvoiceController (клиент) + AdminInvoiceController (список+mark-paid)
- Модели SaasInvoice/SaasInvoiceItem/SaasUpdDocument; шаблоны pdf/invoice|act
- Нумерация СЧ-ГГГГ-NNNNN (advisory-lock); просрочка invoices:expire (cron)
- Наименование услуги: «Оплата генерации рекламных лидов»
- Зависимость barryvdh/laravel-dompdf (default_font dejavu sans); схема БД не менялась
- Этап 2 (автомат через ВТБ API) — отдельно, спека/план в docs/superpowers

Тесты: счета 13, Billing 138, фронт зелёные; larastan baseline +6 (Pest false-pos).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 11:32:21 +03:00

50 lines
2.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\LegalEntity;
use App\Models\SaasInvoice;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Storage;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
function adminSeedInvoice(int $tenantId, int $legalEntityId, string $status, string $amount, string $number): SaasInvoice
{
return SaasInvoice::create([
'tenant_id' => $tenantId, 'legal_entity_id' => $legalEntityId, 'invoice_number' => $number,
'payer_type' => 'legal', 'payer_name' => 'ООО Клиент', 'payer_inn' => '5000000000',
'amount_net' => $amount, 'amount_total' => $amount, 'status' => $status,
'issued_at' => now(), 'expires_at' => now()->addDays(5),
]);
}
it('GET /api/admin/invoices отдаёт выставленные счета с пагинацией', function () {
$tenant = Tenant::factory()->create();
$le = LegalEntity::create(['code' => 'al_'.uniqid(), 'name' => 'ИП', 'legal_form' => 'IP', 'inn' => '770000000010', 'is_default' => true]);
adminSeedInvoice($tenant->id, $le->id, 'issued', '100.00', 'СЧ-2026-01001');
adminSeedInvoice($tenant->id, $le->id, 'issued', '200.00', 'СЧ-2026-01002');
adminSeedInvoice($tenant->id, $le->id, 'paid', '300.00', 'СЧ-2026-01003');
$this->getJson('/api/admin/invoices?status=issued')
->assertOk()
->assertJsonStructure(['data' => [['id', 'invoice_number', 'amount_total', 'status', 'tenant_id']], 'meta' => ['total']])
->assertJsonPath('meta.total', 2);
});
it('POST /api/admin/invoices/{id}/mark-paid зачисляет баланс и ставит paid', function () {
Storage::fake('local');
$tenant = Tenant::factory()->create(['balance_rub' => '0.00']);
User::factory()->create(['tenant_id' => $tenant->id]);
$le = LegalEntity::create(['code' => 'al2_'.uniqid(), 'name' => 'ИП', 'legal_form' => 'IP', 'inn' => '770000000011', 'is_default' => true]);
$invoice = adminSeedInvoice($tenant->id, $le->id, 'issued', '700.00', 'СЧ-2026-01010');
$this->postJson("/api/admin/invoices/{$invoice->id}/mark-paid")->assertOk();
expect((string) $tenant->fresh()->balance_rub)->toBe('700.00')
->and(SaasInvoice::find($invoice->id)->status)->toBe('paid');
});