Files
portal/app/tests/Feature/Billing/InvoiceCreateTest.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

103 lines
3.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\LegalEntity;
use App\Models\SaasInvoice;
use App\Models\Tenant;
use App\Models\TenantRequisites;
use App\Models\User;
use App\Services\Billing\Invoice\InvoiceService;
use App\Services\Billing\Invoice\RequisitesIncompleteException;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Storage;
uses(DatabaseTransactions::class);
function makeSellerLe(): LegalEntity
{
return LegalEntity::create([
'code' => 'seller_'.uniqid(), 'name' => 'ИП Лидерра', 'legal_form' => 'IP',
'inn' => '770000000001', 'bank_name' => 'ВТБ', 'bank_bik' => '044525187',
'bank_account' => '40802810000000000001', 'bank_corr' => '30101810700000000187',
'is_default' => true,
]);
}
function makeClientRequisites(int $tenantId): TenantRequisites
{
return TenantRequisites::create([
'tenant_id' => $tenantId,
'subject_type' => 'legal_entity',
'contact_name' => 'Иван Клиентов',
'contact_phone' => '+79150000000',
'inn' => '5000000000',
'legal_name' => 'ООО Клиент',
'kpp' => '500001001',
'legal_address' => 'г. Москва, ул. Пример, 1',
'bank_account' => '40702810000000000002',
]);
}
it('создаёт счёт issued с позицией, без НДС, номером и PDF', function () {
Storage::fake('local');
$tenant = Tenant::factory()->create();
makeSellerLe();
makeClientRequisites($tenant->id);
$invoice = app(InvoiceService::class)->create($tenant->id, '1500.00', null);
expect($invoice->status)->toBe(SaasInvoice::STATUS_ISSUED)
->and((string) $invoice->amount_total)->toBe('1500.00')
->and((float) $invoice->vat_amount)->toBe(0.0)
->and($invoice->invoice_number)->toStartWith('СЧ-')
->and($invoice->pdf_path)->not->toBeNull()
->and($invoice->payer_name)->toBe('ООО Клиент')
->and($invoice->items()->count())->toBe(1)
->and($invoice->payment_purpose)->toContain($invoice->invoice_number);
Storage::disk('local')->assertExists($invoice->pdf_path);
});
it('бросает доменную ошибку если реквизиты клиента не заполнены', function () {
$tenant = Tenant::factory()->create();
makeSellerLe();
app(InvoiceService::class)->create($tenant->id, '1500.00', null);
})->throws(RequisitesIncompleteException::class);
it('POST /api/billing/invoices создаёт счёт и возвращает 201 с pdf-ссылкой', function () {
Storage::fake('local');
$tenant = Tenant::factory()->create();
makeSellerLe();
makeClientRequisites($tenant->id);
$this->actingAs(User::factory()->create(['tenant_id' => $tenant->id]));
$this->postJson('/api/billing/invoices', ['amount_rub' => 2000])
->assertStatus(201)
->assertJsonStructure(['invoice' => ['id', 'invoice_number', 'amount_total', 'pdf_url']]);
});
it('POST /api/billing/invoices без реквизитов → 422', function () {
Storage::fake('local');
$tenant = Tenant::factory()->create();
makeSellerLe();
$this->actingAs(User::factory()->create(['tenant_id' => $tenant->id]));
$this->postJson('/api/billing/invoices', ['amount_rub' => 2000])->assertStatus(422);
});
it('GET /api/billing/invoices/{id}/pdf скачивает PDF своего счёта', function () {
Storage::fake('local');
$tenant = Tenant::factory()->create();
makeSellerLe();
makeClientRequisites($tenant->id);
$this->actingAs(User::factory()->create(['tenant_id' => $tenant->id]));
$invoice = app(InvoiceService::class)->create($tenant->id, '2000.00', null);
$this->get("/api/billing/invoices/{$invoice->id}/pdf")
->assertOk()
->assertHeader('content-type', 'application/pdf');
});