Files
portal/app/app/Mail/SupplierCriticalAlertMail.php
T
Дмитрий dedaae5aaa feat(supplier): Plan 3 Task 6 — SyncSupplierProjectsJob + SupplierQuotaAllocator
Компоненты:
- SupplierQuotaAllocator: pure function distribution-логики
  - site/call: B1=ceil(t/3), B2=ceil(r/2), B3=remainder
  - sms-with-keyword: B2+B3 only (B1=0, spec §2.2 — B1 не поддерживает СМС)
  - Workdays/regions union, weekday-фильтрация по Europe/Moscow
  - Возвращает null когда нет projects на targetWeekday
- SyncSupplierProjectsJob: 20:30 МСК cron
  - SupplierProject::on('pgsql_supplier') — cross-tenant видимость
  - whereNull('inactive_since') — sync только активные
  - Адаптер Project → stdClass: daily_limit_target → daily_limit,
    delivery_days_mask bits → workdays, region_mask bits → regions
    (mask=255 catch-all → regions=[])
  - per-supplier_project failure-isolation (continue на one bad)
  - mass-fail abort: 50 consecutive transient → SupplierCriticalAlertMail
    + Sentry + break
  - sticky auth → email('sticky_auth') + Sentry + throw
  - time budget cutoff 20:55 МСК (5-мин safety margin до 21:00)
  - supplier_sync_log per action (action='create'/'update', http_status,
    error_message)
- SupplierCriticalAlertMail: ShouldQueue Mailable + text template
  - Unisender Go SMTP relay через config('services.supplier.alert_email')

NOTE про connection: следуем Task 3 learning — не используем public \$connection
(это queue connection, не DB). Queries через Model::on('pgsql_supplier').

NOTE про DB::transaction: НЕ оборачиваем syncOne, т.к. HTTP-call к supplier
выходит за границы транзакции (атомарности всё равно нет). Два DB-write
последовательно; ошибка между ними recoverable через retry на следующем cron-tick
(supplier_external_id уже записан, скип через SupplierProjectDto::equals()).

+18 тестов (10 allocator + 8 sync job).

phpstan-baseline.neon: +7 entries для PHPStan template-covariance issue в
SupplierQuotaAllocatorTest — \`Collection<int, object{...literal}&stdClass>\` не
suptype \`Collection<int, stdClass>\` per PHPStan invariance rule. Production
code clean (0 baseline entries).
2026-05-11 06:46:13 +03:00

52 lines
1.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
/**
* Critical alert email при отказе supplier-sync.
*
* Триггеры (Plan 3 Task 6 SyncSupplierProjectsJob):
* - sticky_auth: SupplierAuthException после retry через RefreshSupplierSessionJob
* - mass_transient: 50+ последовательных SupplierTransientException (подозрение на outage)
*
* Отправляется через config('services.supplier.alert_email') (Unisender Go SMTP relay).
*
* Spec: docs/superpowers/specs/2026-05-11-plan3-supplier-sync-design.md §4 Email alerts.
*/
final class SupplierCriticalAlertMail extends Mailable implements ShouldQueue
{
use Queueable;
public function __construct(
public readonly string $alertType,
public readonly string $details,
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: "[Лидерра CRITICAL] Supplier sync: {$this->alertType}",
);
}
public function content(): Content
{
return new Content(
text: 'emails.supplier_alert_text',
with: [
'alertType' => $this->alertType,
'details' => $this->details,
'now' => now()->timezone('Europe/Moscow')->toIso8601String(),
],
);
}
}