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>
101 lines
4.2 KiB
PHP
101 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\SupplierProject;
|
|
use App\Services\Supplier\SupplierPortalClient;
|
|
use Illuminate\Console\Command;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Разовая починка после прод-инцидента 11–12.07.2026.
|
|
*
|
|
* Кабинет поставщика дописывает метку канала «B<n>_» к имени строки только при её
|
|
* СОЗДАНИИ; при обновлении он сохраняет имя ровно как прислали. Наш ночной робот слал
|
|
* голый ключ → каждый прогон стирал метку. Последствия: (1) итоговая проверка заказа
|
|
* переставала узнавать строку и слала ложное «нет заказа»; (2) лид от такой строки
|
|
* приходил с project без метки → webhook не мог определить канал и писал
|
|
* platform=DIRECT вместо B1/B2/B3 (потеря атрибуции канала).
|
|
*
|
|
* Корень закрыт в SupplierPortalClient::toPayload (на update имя уходит с меткой) —
|
|
* эта команда возвращает метку тем строкам, у которых её уже стёрли.
|
|
*
|
|
* По умолчанию — сухой прогон. Реальная запись в кабинет — только с --apply.
|
|
*/
|
|
final class RepairSupplierProjectNamesCommand extends Command
|
|
{
|
|
private const DB_CONNECTION = 'pgsql_supplier';
|
|
|
|
/** Канал у поставщика — служебное поле src (rt=Ростелеком, bl=Билайн, mt=МТС). */
|
|
private const SRC_TO_PLATFORM = ['rt' => 'B1', 'bl' => 'B2', 'mt' => 'B3'];
|
|
|
|
protected $signature = 'supplier:repair-project-names
|
|
{--apply : Записать исправленные имена в кабинет (без флага — только показать)}
|
|
{--only-active : Чинить лишь строки, включённые у нас (выключенные лидов не носят)}';
|
|
|
|
protected $description = 'Вернуть метку канала B1_/B2_/B3_ в имена наших строк у поставщика';
|
|
|
|
public function handle(SupplierPortalClient $client): int
|
|
{
|
|
$apply = (bool) $this->option('apply');
|
|
|
|
$query = SupplierProject::on(self::DB_CONNECTION)
|
|
->whereNotNull('supplier_external_id');
|
|
|
|
if ($this->option('only-active')) {
|
|
$query->whereNull('inactive_since');
|
|
}
|
|
|
|
/** @var array<string, true> $ourIds */
|
|
$ourIds = $query->pluck('supplier_external_id')
|
|
->mapWithKeys(fn ($id): array => [(string) $id => true])
|
|
->all();
|
|
|
|
$repaired = 0;
|
|
$failed = 0;
|
|
|
|
foreach ($client->listProjects() as $row) {
|
|
$externalId = (string) ($row['id'] ?? '');
|
|
if (! isset($ourIds[$externalId])) {
|
|
continue;
|
|
}
|
|
|
|
$platform = self::SRC_TO_PLATFORM[(string) ($row['src'] ?? '')] ?? null;
|
|
if ($platform === null) {
|
|
continue;
|
|
}
|
|
|
|
$name = (string) ($row['name'] ?? '');
|
|
if (preg_match('/^B[123]_/', $name) === 1) {
|
|
continue; // Метка на месте — не трогаем.
|
|
}
|
|
|
|
$newName = $platform.'_'.(string) ($row['content'] ?? '');
|
|
$this->line(sprintf('%s %s «%s» → «%s»', $externalId, $platform, $name, $newName));
|
|
|
|
if (! $apply) {
|
|
$repaired++;
|
|
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$client->renameProject($row, $newName);
|
|
$repaired++;
|
|
} catch (Throwable $e) {
|
|
$failed++;
|
|
$this->error(sprintf(' не удалось: %s', $e->getMessage()));
|
|
}
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info($apply
|
|
? sprintf('Починено строк: %d, ошибок: %d.', $repaired, $failed)
|
|
: sprintf('Сухой прогон: строк с потерянной меткой — %d. Записать: --apply.', $repaired));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|