ce87936f44
При InsufficientBalanceException в LedgerService::chargeForDelivery: - DB::transaction откатывается (Deal/charge/balance не тронуты). - Outer catch в createDealCopyForProject вызывает handleInsufficientBalance: * UPDATE projects.is_active=false через pgsql_supplier (BYPASSRLS). * Email ZeroBalancePausedMail через NotificationService::notifyZeroBalancePaused. * Rate-limit 1/час/tenant через Redis SETNX (Cache::add). * Log::warning с tenant_id/project_id/balance details. - Возвращаем false (не rethrow), чтобы handle()-loop продолжал routing остальным tenant'ам. 5 тестов: project paused / email sent / rate-limit 1/h / 2nd email after 65min / sharing-flow isolation (A paused, B receives). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 lines
1.4 KiB
PHP
48 lines
1.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Mail;
|
||
|
||
use App\Models\Project;
|
||
use App\Models\Tenant;
|
||
use Illuminate\Bus\Queueable;
|
||
use Illuminate\Mail\Mailable;
|
||
use Illuminate\Mail\Mailables\Content;
|
||
use Illuminate\Mail\Mailables\Envelope;
|
||
use Illuminate\Queue\SerializesModels;
|
||
|
||
/**
|
||
* Email клиенту Лидерры о приостановке проекта из-за недостаточного баланса.
|
||
*
|
||
* Триггер: RouteSupplierLeadJob::handleInsufficientBalance ловит
|
||
* InsufficientBalanceException, помечает projects.is_active=false и шлёт это
|
||
* письмо с rate-limit 1/час/tenant (Redis SETNX).
|
||
*
|
||
* Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §4.4
|
||
*/
|
||
final class ZeroBalancePausedMail extends Mailable
|
||
{
|
||
use Queueable;
|
||
use SerializesModels;
|
||
|
||
public function __construct(
|
||
public readonly Tenant $tenant,
|
||
public readonly Project $project,
|
||
public readonly int $requiredPriceKopecks,
|
||
) {}
|
||
|
||
public function envelope(): Envelope
|
||
{
|
||
return new Envelope(
|
||
subject: "Проект «{$this->project->name}» приостановлен — недостаточно средств",
|
||
to: [$this->tenant->contact_email],
|
||
);
|
||
}
|
||
|
||
public function content(): Content
|
||
{
|
||
return new Content(view: 'emails.zero_balance_paused');
|
||
}
|
||
}
|