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:
Дмитрий
2026-06-29 11:25:16 +03:00
parent 67095f564d
commit a7b123a7c1
42 changed files with 4146 additions and 205 deletions
+9
View File
@@ -139,6 +139,11 @@ Route::middleware(['saas-admin', 'admin-db'])->group(function () {
// SaaS-admin → Биллинг: aggregates пополнений/списаний за текущий месяц.
Route::get('/api/admin/billing', 'App\Http\Controllers\Api\AdminBillingController@index');
// SaaS-admin → Счета: список выставленных счетов + ручная отметка оплаты (Этап 1).
Route::get('/api/admin/invoices', 'App\Http\Controllers\Api\AdminInvoiceController@index');
Route::post('/api/admin/invoices/{id}/mark-paid', 'App\Http\Controllers\Api\AdminInvoiceController@markPaid')
->whereNumber('id');
// Sprint 3D (G4): SaaS-admin billing row-actions — приостановка/возврат/смена тарифа.
Route::get('/api/admin/billing/tariff-plans', 'App\Http\Controllers\Api\AdminBillingController@tariffPlans');
Route::patch('/api/admin/billing/tenants/{id}/status', 'App\Http\Controllers\Api\AdminBillingController@updateStatus')
@@ -238,6 +243,9 @@ Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/billing')->group(fun
Route::get('/balance-status', 'App\Http\Controllers\Api\BillingController@balanceStatus');
Route::get('/transactions', 'App\Http\Controllers\Api\BillingController@transactions');
Route::get('/invoices', 'App\Http\Controllers\Api\BillingController@invoices');
Route::post('/invoices', 'App\Http\Controllers\Api\InvoiceController@store');
Route::get('/invoices/{id}/pdf', 'App\Http\Controllers\Api\InvoiceController@pdf')->whereNumber('id');
Route::get('/invoices/{id}/act', 'App\Http\Controllers\Api\InvoiceController@act')->whereNumber('id');
});
// API-ключи тенанта (audit D2/D3/J5). RLS на api_keys требует tenant middleware.
@@ -381,6 +389,7 @@ Route::view('/import', 'welcome'); // Sprint 4 — CSV-импорт истори
Route::view('/admin', 'welcome');
Route::view('/admin/tenants', 'welcome');
Route::view('/admin/billing', 'welcome');
Route::view('/admin/invoices', 'welcome');
Route::view('/admin/incidents', 'welcome');
Route::view('/admin/system', 'welcome');
Route::view('/admin/pricing-tiers', 'welcome');