4c33323f0e
P0 этап 6 — 4 оставшихся email-события. Авто-план P0 (6 этапов) закрыт полностью: все 8 schema-default событий имеют рабочую интеграцию (new_lead/reminder/low_balance/zero_balance/topup_success/invoice_paid + заглушки для new_device_login/marketing). Backend: - 4 новых Mailable: LowBalanceNotification (threshold), ZeroBalanceNotification, TopupSuccessNotification (amountRub, amountLeads?), InvoicePaidNotification (amountRub, invoiceNumber?, tariffName?). - 4 blade-шаблона в emails/ (Forest-палитра, таблицы balance/amount/invoice). - NotificationService +4 методов: notifyLowBalance / notifyZeroBalance / notifyTopupSuccess / notifyInvoicePaid. Все шлют email + inapp по prefs. Интеграция в ProcessWebhookJob: - chargeNewLead после lead_charge: notifyLowBalance при пересечении порога сверху-вниз (balance_after <= threshold AND (balance_after+1) > threshold). Иначе спам при каждом lead_charge при balance < threshold. - logRejection(zero_balance): notifyZeroBalance ТОЛЬКО если в последний час не было другого RejectedDealsLog с тем же reason (anti-spam 1 email/час). Защита от self-just-inserted через id!= (timestamp-сравнение ненадёжно из-за PG microsecond precision). - topup_success / invoice_paid — service-методы готовы, integration после появления endpoints для пополнения (ЮKassa-webhook) и оплаты тарифа. - lowBalanceThreshold() читает system_settings.low_balance_threshold_leads (default 10, schema seed). Pest +12 в BalanceNotificationsTest (359/359 за 41.37 сек, 1233 assertions): - low_balance: пересечение порога / уже < threshold / > threshold / prefs.email=false (только inapp). - zero_balance: первое отклонение / 2-е в час не дублирует / >1ч снова шлёт. - topup_success / invoice_paid: notify создаёт email+inapp / prefs=email:false. - balance events изолированы между tenants. NewLeadNotificationTest: «balance=0 не шлёт» обновлён — Mail::assertNotSent(NewLeadNotification) вместо Mail::assertNothingSent (ZeroBalanceNotification теперь шлётся при balance=0 — новое поведение). PHPStan baseline регенерирован. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
51 lines
1.3 KiB
PHP
51 lines
1.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Mail;
|
|
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Mail\Mailable;
|
|
use Illuminate\Mail\Mailables\Content;
|
|
use Illuminate\Mail\Mailables\Envelope;
|
|
use Illuminate\Queue\SerializesModels;
|
|
|
|
/**
|
|
* Email-уведомление о нулевом балансе и отклонении лидов (ТЗ §18.5,
|
|
* событие zero_balance).
|
|
*
|
|
* Триггер: ProcessWebhookJob::logRejection(reason=zero_balance) — после
|
|
* первого RejectedDealsLog в течение последнего часа (anti-spam: не больше
|
|
* 1 email в час на тенант).
|
|
*/
|
|
class ZeroBalanceNotification extends Mailable
|
|
{
|
|
use Queueable;
|
|
use SerializesModels;
|
|
|
|
public function __construct(
|
|
public User $recipient,
|
|
public Tenant $tenant,
|
|
) {}
|
|
|
|
public function envelope(): Envelope
|
|
{
|
|
return new Envelope(
|
|
subject: 'Лидерра. Баланс закончился — лиды отклоняются',
|
|
);
|
|
}
|
|
|
|
public function content(): Content
|
|
{
|
|
return new Content(
|
|
view: 'emails.zero_balance',
|
|
with: [
|
|
'recipient' => $this->recipient,
|
|
'tenant' => $this->tenant,
|
|
],
|
|
);
|
|
}
|
|
}
|