8c1d1710fb
Прод-инцидент 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>
129 lines
5.8 KiB
PHP
129 lines
5.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\SupplierProject;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Http\Client\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Tests\Concerns\SharesSupplierPdo;
|
|
|
|
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
|
|
|
/*
|
|
* Разовая починка после прод-инцидента 11–12.07.2026: наш ежедневный updateProject слал
|
|
* имя без метки канала, и кабинет стирал метку «B<n>_». Строка без метки → лид от неё
|
|
* приходит с project без метки → webhook пишет platform=DIRECT вместо B1/B2/B3.
|
|
*
|
|
* Команда возвращает метку на место. Payload собирается ИЗ ЖИВОЙ СТРОКИ кабинета
|
|
* (лимит/дни/регионы/вкл-выкл — как есть), меняется ровно одно поле — name. Регионы
|
|
* из кабинета уже в кодах поставщика, повторно их переводить нельзя.
|
|
*/
|
|
|
|
beforeEach(function (): void {
|
|
Cache::store('redis')->put('supplier:session', [
|
|
'phpsessid' => 'test-session',
|
|
'csrf' => 'test-csrf',
|
|
], now()->addHour());
|
|
|
|
config(['services.supplier.portal_url' => 'https://crm.bp-gr.ru']);
|
|
});
|
|
|
|
/** @param array<int, array<string,mixed>> $rows */
|
|
function fakeCabinet(array $rows): void
|
|
{
|
|
Http::fake([
|
|
'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(['projects' => $rows], 200),
|
|
'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(['status' => 'OK', 'message' => '', 'id' => '1'], 200),
|
|
]);
|
|
}
|
|
|
|
/** Строка кабинета с потерянной меткой канала. */
|
|
function bareCabinetRow(string $id, string $src, string $content): array
|
|
{
|
|
return [
|
|
'id' => $id, 'src' => $src, 'tag' => 'РФ', 'name' => $content, 'content' => $content,
|
|
'type' => 'calls', 'lim' => '5', 'status' => true, 'workdays' => ['1', '2', '3', '4', '5'],
|
|
'regions' => '24;66', 'regions_reverse' => false,
|
|
];
|
|
}
|
|
|
|
it('dry-run: показывает строки с потерянной меткой, но НИЧЕГО не пишет в кабинет', function (): void {
|
|
SupplierProject::factory()->create([
|
|
'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
|
'supplier_external_id' => '6001', 'inactive_since' => null,
|
|
]);
|
|
fakeCabinet([bareCabinetRow('6001', 'rt', '79135397707')]);
|
|
|
|
$this->artisan('supplier:repair-project-names')
|
|
->expectsOutputToContain('B1_79135397707')
|
|
->assertExitCode(0);
|
|
|
|
Http::assertNotSent(fn (Request $r): bool => str_contains($r->url(), 'rt-project-save'));
|
|
});
|
|
|
|
it('--apply: возвращает метку канала, сохраняя лимит/дни/регионы/вкл-выкл как есть', function (): void {
|
|
SupplierProject::factory()->create([
|
|
'platform' => 'B2', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
|
'supplier_external_id' => '6002', 'inactive_since' => null,
|
|
]);
|
|
fakeCabinet([bareCabinetRow('6002', 'bl', '79135397707')]);
|
|
|
|
$this->artisan('supplier:repair-project-names', ['--apply' => true])->assertExitCode(0);
|
|
|
|
Http::assertSent(function (Request $r): bool {
|
|
if (! str_contains($r->url(), 'rt-project-save')) {
|
|
return false;
|
|
}
|
|
|
|
return $r['id'] === 6002
|
|
&& $r['name'] === 'B2_79135397707'
|
|
&& $r['content'] === '79135397707'
|
|
&& $r['srcbl'] === true && $r['srcrt'] === false && $r['srcmt'] === false
|
|
&& $r['limit'] === 5
|
|
&& $r['status'] === true
|
|
&& $r['workdays'] === ['1', '2', '3', '4', '5']
|
|
// Регионы из кабинета — уже коды поставщика, отдаём обратно без перевода.
|
|
&& $r['regions'] === [24, 66];
|
|
});
|
|
});
|
|
|
|
it('не трогает строки, у которых метка на месте, и чужие строки кабинета', function (): void {
|
|
SupplierProject::factory()->create([
|
|
'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
|
'supplier_external_id' => '6003', 'inactive_since' => null,
|
|
]);
|
|
|
|
$withPrefix = bareCabinetRow('6003', 'rt', '79135397707');
|
|
$withPrefix['name'] = 'B1_79135397707';
|
|
$foreign = bareCabinetRow('9999', 'rt', 'chужой.ru'); // не наш external_id
|
|
|
|
fakeCabinet([$withPrefix, $foreign]);
|
|
|
|
$this->artisan('supplier:repair-project-names', ['--apply' => true])->assertExitCode(0);
|
|
|
|
Http::assertNotSent(fn (Request $r): bool => str_contains($r->url(), 'rt-project-save'));
|
|
});
|
|
|
|
it('--only-active: чинит только включённые у нас строки (выключенные лидов не носят)', function (): void {
|
|
SupplierProject::factory()->create([
|
|
'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
|
'supplier_external_id' => '6004', 'inactive_since' => null,
|
|
]);
|
|
SupplierProject::factory()->create([
|
|
'platform' => 'B3', 'signal_type' => 'call', 'unique_key' => '79990001122',
|
|
'supplier_external_id' => '6005', 'inactive_since' => now(),
|
|
]);
|
|
fakeCabinet([
|
|
bareCabinetRow('6004', 'rt', '79135397707'),
|
|
bareCabinetRow('6005', 'mt', '79990001122'),
|
|
]);
|
|
|
|
$this->artisan('supplier:repair-project-names', ['--apply' => true, '--only-active' => true])
|
|
->assertExitCode(0);
|
|
|
|
Http::assertSent(fn (Request $r): bool => str_contains($r->url(), 'rt-project-save') && $r['id'] === 6004);
|
|
Http::assertNotSent(fn (Request $r): bool => str_contains($r->url(), 'rt-project-save') && $r['id'] === 6005);
|
|
});
|