Files
portal/app/app/Mail/CsvDriftAlertMail.php
T
Дмитрий deca81c2d7 feat(supplier): Plan 4 Task 8 — CsvReconcileJob hourly + drift>5% email + supplier_csv_reconcile_log
CsvReconcileJob — hourly резерв-канал приёма лидов через CSV-экспорт поставщика:
- Cache::lock 600s (overlap protection).
- Окно [now-25h, now] (запас 1ч над hourly cron).
- INSERT supplier_csv_reconcile_log status='running' → 'ok' | 'drift_alert' | 'failed'.
- Missing vids → INSERT supplier_leads (platform extracted из project, source='csv_recovery',
  recovered_from_csv_at=now) + dispatch RouteSupplierLeadJob.
- Drift > 5% → CsvDriftAlertMail на services.supplier.alert_email.
- UNIQUE-vid conflict → log + skip (idempotency).
- На SupplierTransientException/любой Throwable → status='failed', error_message, rethrow.

CsvDriftAlertMail + blade-template emails/csv_drift_alert.

routes/console.php — Schedule::job(new CsvReconcileJob)->hourly().
config/services.php — supplier.alert_email default 'ops@liderra.ru'.

6 integration tests (CsvReconcileJobTest) + Schedule registration test (через
Http::fake + Bus::fake + Mail::fake + SharesSupplierPdo trait для cross-connection).

Parallel-test race fix: putSupplierSession() вызывается прямо перед SUT, потому
что Sync/Cleanup tests'ы в afterEach делают forget('supplier:session'), а в
--parallel режиме воркеры делят Redis DB+prefix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:04:49 +03:00

49 lines
1.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Mail;
use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
/**
* Email алерт админу Лидерры о расхождении CSV-сверки > 5%.
*
* Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §5.6
*/
final class CsvDriftAlertMail extends Mailable
{
use Queueable;
use SerializesModels;
public function __construct(
public readonly int $reconcileLogId,
public readonly int $totalCsvRows,
public readonly int $missingCount,
public readonly int $recoveredCount,
public readonly float $driftRatio,
public readonly CarbonInterface $windowStart,
public readonly CarbonInterface $windowEnd,
) {}
public function envelope(): Envelope
{
$pct = number_format($this->driftRatio * 100, 2, ',', ' ');
$window = $this->windowStart->format('Y-m-d H:i').' — '.$this->windowEnd->format('Y-m-d H:i');
return new Envelope(
subject: "Лидерра ↔ Поставщик: расхождение CSV > 5% за {$window} ({$pct}%)",
);
}
public function content(): Content
{
return new Content(view: 'emails.csv_drift_alert');
}
}