Files
portal/app/app/Console/Commands/RepairSupplierProjectNamesCommand.php
T
Дмитрий b28f858419 fix(supplier): метка канала B1_/B2_/B3_ не стирается при обновлении заказа
Прод-инцидент 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>
2026-07-12 19:14:22 +03:00

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;
}
}