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

70 lines
2.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Mail\InvoicePaidNotification;
use App\Models\LegalEntity;
use App\Models\SaasInvoice;
use App\Models\SaasTransaction;
use App\Models\SaasUpdDocument;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Billing\Invoice\InvoicePaymentService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
uses(DatabaseTransactions::class);
function seedPaidScenario(string $balance, string $amount): array
{
$tenant = Tenant::factory()->create(['balance_rub' => $balance]);
User::factory()->create(['tenant_id' => $tenant->id]);
$le = 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-00777', 'payer_type' => 'legal', 'payer_name' => 'ООО К',
'payer_inn' => '5000000000', 'amount_net' => $amount, 'amount_total' => $amount,
'status' => SaasInvoice::STATUS_ISSUED, 'issued_at' => now(), 'expires_at' => now()->addDays(5),
]);
return [$tenant, $invoice];
}
it('mark-paid зачисляет баланс, ставит paid, создаёт акт и шлёт письмо', function () {
Storage::fake('local');
Mail::fake();
[$tenant, $invoice] = seedPaidScenario('100.00', '1500.00');
app(InvoicePaymentService::class)->markPaid($invoice->id);
$invoice->refresh();
$tenant->refresh();
expect($invoice->status)->toBe(SaasInvoice::STATUS_PAID)
->and($invoice->paid_at)->not->toBeNull()
->and((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);
$actPath = SaasUpdDocument::where('invoice_id', $invoice->id)->value('pdf_path');
Mail::assertQueued(InvoicePaidNotification::class, fn ($mail) => $mail->actPdfPath === $actPath
&& count($mail->attachments()) === 1);
});
it('повторный mark-paid идемпотентен — баланс не удваивается, второй акт не создаётся', function () {
Storage::fake('local');
Mail::fake();
[$tenant, $invoice] = seedPaidScenario('0.00', '500.00');
$svc = app(InvoicePaymentService::class);
$svc->markPaid($invoice->id);
$svc->markPaid($invoice->id);
$tenant->refresh();
expect((string) $tenant->balance_rub)->toBe('500.00')
->and(SaasUpdDocument::where('invoice_id', $invoice->id)->count())->toBe(1);
});