b28f858419
Прод-инцидент 11-12.07.2026: робот итоговой проверки слал письма «нет заказа у
поставщика» на строки, которые в кабинете ЕСТЬ, включены и с верным лимитом.
Корень: кабинет дописывает метку канала к имени строки только при СОЗДАНИИ, а при
обновлении сохраняет имя ровно как прислали. Наш ежедневный updateProject слал голый
uniqueKey и каждый прогон стирал метку. Последствия:
- итоговая проверка выводила площадку из префикса имени и переставала узнавать
строку -> ложное missing 11.07 и 12.07;
- лид от такой строки приходил с project без метки -> webhook не мог определить
канал и писал platform=DIRECT вместо B1/B2/B3, то есть терялась атрибуция канала.
Что сделано:
- SupplierPortalClient::toPayload — на update имя уходит с меткой канала; на create
остаётся голым, там метку ставит сам кабинет и один save с тремя флагами рождает
три строки, общего префикса у них нет.
- VerifySupplierOrderJob::normalizeLive — площадка берётся из служебного поля src
rt/bl/mt, а не из префикса имени; сверка больше не зависит от имени вообще.
- Новая разовая команда supplier:repair-project-names — возвращает метку строкам,
у которых её уже стёрли. Payload собирается ИЗ ЖИВОЙ строки кабинета, меняется
ровно одно поле name; по умолчанию сухой прогон, запись только с --apply.
Ветка пересобрана на gitea/main — закрывает follow-up «фича итоговой проверки заказа
не сведена в main». Попутно возвращён CsvReconcileJobTest, отставший от кода после
сведения main 09.07: он не фейкал fetchDeliveredLeads и падал 9 из 11.
Боевой liderra.ru: выкачено, починена 81 строка, робот показывает 0 расхождений
138 наших строк вместо 57. Двум лидам восстановлен канал по журналу выдач поставщика.
Тесты: Pest supplier 277/277, Pint clean, Larastan 0 новых ошибок.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
53 lines
2.1 KiB
PHP
53 lines
2.1 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Mail\SupplierDeadlineWarningMail;
|
||
use Carbon\Carbon;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Mail;
|
||
|
||
/**
|
||
* Сторож дедлайна поставщика: если робот SyncSupplierProjectsJob к порогу не закончил —
|
||
* письмо. yellow=20:00, red=20:40 (МСК). Ловит и медленную работу, и зависание, и
|
||
* несработавший планировщик (за сегодня нет завершённого запуска).
|
||
*
|
||
* Spec: docs/superpowers/specs/2026-07-09-supplier-order-verification-and-deadline-watch-design.md §5
|
||
*/
|
||
final class SupplierDeadlineWatchCommand extends Command
|
||
{
|
||
protected $signature = 'supplier:deadline-watch {level : yellow|red}';
|
||
|
||
protected $description = 'Проверить, успел ли робот-заказчик до дедлайна поставщика (21:00 МСК)';
|
||
|
||
public function handle(): int
|
||
{
|
||
/** @var 'yellow'|'red' $level */
|
||
$level = $this->argument('level') === 'red' ? 'red' : 'yellow';
|
||
$today = Carbon::today('Europe/Moscow')->toDateString();
|
||
|
||
$finished = DB::connection('pgsql_supplier')->table('supplier_sync_runs')
|
||
->whereRaw("(started_at AT TIME ZONE 'Europe/Moscow')::date = ?", [$today])
|
||
->whereNotNull('finished_at')
|
||
->where('status', '!=', 'aborted')
|
||
->exists();
|
||
|
||
if ($finished) {
|
||
$this->info('OK: робот сегодня завершил заказ.');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$reason = 'За сегодня нет завершённого (не aborted) запуска SyncSupplierProjectsJob с finished_at.';
|
||
Mail::to((string) config('services.supplier.alert_email'))
|
||
->queue(new SupplierDeadlineWarningMail($level, $reason));
|
||
|
||
$this->warn("{$level}: робот не закончил — письмо отправлено.");
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
}
|