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>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\LegalEntity;
|
||||
use App\Models\SaasInvoice;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Billing\Invoice\InvoiceNumberGenerator;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
|
||||
function makeLeForNumbering(): LegalEntity
|
||||
{
|
||||
return LegalEntity::create([
|
||||
'code' => 'le_num_'.uniqid(), 'name' => 'ИП Тест', 'legal_form' => 'IP', 'inn' => '770000000000',
|
||||
]);
|
||||
}
|
||||
|
||||
function seedNumberingInvoice(int $tenantId, int $legalEntityId, string $number, string $issuedAt): SaasInvoice
|
||||
{
|
||||
return SaasInvoice::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'legal_entity_id' => $legalEntityId,
|
||||
'invoice_number' => $number,
|
||||
'payer_type' => 'legal',
|
||||
'amount_net' => '100.00',
|
||||
'amount_total' => '100.00',
|
||||
'status' => SaasInvoice::STATUS_ISSUED,
|
||||
'issued_at' => $issuedAt,
|
||||
'expires_at' => $issuedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
it('первый счёт юрлица за год получает номер -00001', function () {
|
||||
$le = makeLeForNumbering();
|
||||
$num = (new InvoiceNumberGenerator)->next($le->id, Carbon::parse('2026-06-29 12:00:00'));
|
||||
expect($num)->toBe('СЧ-2026-00001');
|
||||
});
|
||||
|
||||
it('следующий номер инкрементируется по существующим счетам того же юрлица/года', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$le = makeLeForNumbering();
|
||||
seedNumberingInvoice($tenant->id, $le->id, 'СЧ-2026-00007', '2026-03-01 00:00:00');
|
||||
|
||||
$num = (new InvoiceNumberGenerator)->next($le->id, Carbon::parse('2026-06-29 12:00:00'));
|
||||
expect($num)->toBe('СЧ-2026-00008');
|
||||
});
|
||||
|
||||
it('нумерация изолирована по юрлицу', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$leA = makeLeForNumbering();
|
||||
$leB = makeLeForNumbering();
|
||||
seedNumberingInvoice($tenant->id, $leA->id, 'СЧ-2026-00042', '2026-02-01 00:00:00');
|
||||
|
||||
expect((new InvoiceNumberGenerator)->next($leB->id, Carbon::parse('2026-06-29 12:00:00')))
|
||||
->toBe('СЧ-2026-00001');
|
||||
});
|
||||
Reference in New Issue
Block a user