cdfae077a3
Клиент сам выставляет 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>
70 lines
2.7 KiB
PHP
70 lines
2.7 KiB
PHP
<?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);
|
||
});
|