deca81c2d7
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>
49 lines
1.4 KiB
PHP
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');
|
|
}
|
|
}
|