cd6875edbb
Разбор докладной 13.07.2026: AutopodborController::restoreMergeEvent воскрешал только карточку поглощённого конкурента (имя/сайт/справочники/телефоны/is_federal), но не его autopodbor_sources — и если у источника был created_project_id (по нему идут заявки/деньги), проект оставался приклеен к выжившей карточке под её именем. Добавлен снимок источников поглощённых конкурентов: nullable-колонка autopodbor_merge_events.absorbed_sources (миграция 2026_07_14_090000, идемпотентна), заполняется AutopodborCompetitorMerger::snapshotAbsorbedSources ДО переноса/ переименования при слиянии (включая имя проекта на момент склейки). restoreMergeEvent по этому снимку либо перевешивает исходную строку источника обратно на воскрешённую карточку, либо пересоздаёт её (если строка была удалена при разрешении коллизии dedup_key), и откатывает имя проекта. Старые записи журнала (до миграции) снимка не имеют — восстанавливаются как раньше (только карточка), ответ API помечается 'partial' => true с честным сообщением. db/schema.sql v8.65 + db/CHANGELOG_schema.md — запись синхронизирована. tests/Feature/Autopodbor/ + tests/Feature/Bot/ — 417/417 после migrate:fresh. Живая проверка на локальном стенде через Playwright (создание проекта → слияние → «Вернуть» → проект и источник вернулись на воскрешённую карточку) — подтверждено. Ветка worktree-jivo-bot-core, НЕ на боевом проде — выкат отдельным решением владельца. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
383 lines
22 KiB
PHP
383 lines
22 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AutopodborCompetitor;
|
|
use App\Models\AutopodborMergeEvent;
|
|
use App\Models\AutopodborRun;
|
|
use App\Models\AutopodborSource;
|
|
use App\Models\Project;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\Concerns\SharesSupplierPdo;
|
|
|
|
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
|
|
|
function dupSetup(): array
|
|
{
|
|
$tenant = Tenant::factory()->create();
|
|
$user = User::factory()->create(['tenant_id' => $tenant->id]);
|
|
DB::statement('SET app.current_tenant_id = '.$tenant->id);
|
|
$run = AutopodborRun::create(['tenant_id' => $tenant->id, 'kind' => 'study', 'status' => 'done', 'region_code' => 16, 'params' => []]);
|
|
|
|
return [$tenant, $user, $run];
|
|
}
|
|
|
|
function dupComp(Tenant $t, AutopodborRun $run, string $name): AutopodborCompetitor
|
|
{
|
|
return AutopodborCompetitor::create([
|
|
'tenant_id' => $t->id, 'search_run_id' => $run->id,
|
|
'name' => $name, 'dedup_key' => 'c:'.md5($name), 'box' => 'field',
|
|
]);
|
|
}
|
|
|
|
function dupCall(Tenant $t, AutopodborRun $run, AutopodborCompetitor $c, string $phone): void
|
|
{
|
|
AutopodborSource::create([
|
|
'tenant_id' => $t->id, 'competitor_id' => $c->id, 'study_run_id' => $run->id,
|
|
'signal_type' => 'call', 'identifier' => $phone, 'dedup_key' => 'call:'.$phone,
|
|
'box' => 'field', 'confirmations' => 1,
|
|
]);
|
|
}
|
|
|
|
it('GET field/duplicate-groups — группирует конкурентов с общим телефоном, отдаёт имена и причину', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$a = dupComp($t, $run, 'AutoMoney');
|
|
$b = dupComp($t, $run, 'Автодом24');
|
|
$c = dupComp($t, $run, 'Другая'); // без общих контактов
|
|
dupCall($t, $run, $a, '79135396546');
|
|
dupCall($t, $run, $b, '79135396546');
|
|
dupCall($t, $run, $c, '79990001122');
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')->assertOk();
|
|
|
|
$resp->assertJsonCount(1, 'groups');
|
|
expect(mb_strtolower($resp->json('groups.0.reason')))->toContain('телефон');
|
|
$ids = collect($resp->json('groups.0.competitors'))->pluck('id')->sort()->values()->all();
|
|
expect($ids)->toBe(collect([$a->id, $b->id])->sort()->values()->all());
|
|
$names = collect($resp->json('groups.0.competitors'))->pluck('name')->all();
|
|
expect($names)->toContain('AutoMoney')->toContain('Автодом24');
|
|
});
|
|
|
|
it('GET field/duplicate-groups?box=proposal — группирует ТОЛЬКО предложения, поле не мешает', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
// Два дубля в предложениях
|
|
$p1 = AutopodborCompetitor::create(['tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Предл A', 'dedup_key' => 'p:a', 'box' => 'proposal']);
|
|
$p2 = AutopodborCompetitor::create(['tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Предл B', 'dedup_key' => 'p:b', 'box' => 'proposal']);
|
|
dupCall($t, $run, $p1, '79135396546');
|
|
dupCall($t, $run, $p2, '79135396546');
|
|
// Дубль в поле — не должен попасть в выборку по box=proposal
|
|
$f1 = dupComp($t, $run, 'Поле A');
|
|
$f2 = dupComp($t, $run, 'Поле B');
|
|
dupCall($t, $run, $f1, '79990001122');
|
|
dupCall($t, $run, $f2, '79990001122');
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')->assertOk();
|
|
|
|
$resp->assertJsonCount(1, 'groups');
|
|
$ids = collect($resp->json('groups.0.competitors'))->pluck('id')->sort()->values()->all();
|
|
expect($ids)->toBe(collect([$p1->id, $p2->id])->sort()->values()->all());
|
|
});
|
|
|
|
function dupProp(Tenant $t, AutopodborRun $run, string $name, array $phones): AutopodborCompetitor
|
|
{
|
|
return AutopodborCompetitor::create([
|
|
'tenant_id' => $t->id, 'search_run_id' => $run->id,
|
|
'name' => $name, 'dedup_key' => 'p:'.md5($name), 'box' => 'proposal', 'phones' => $phones,
|
|
]);
|
|
}
|
|
|
|
it('предложения: общий телефон из сохранённого поля (БЕЗ источников) ловится, причина — тот номер', function () {
|
|
// Шаг 3: у предложений источников нет — телефоны берём из поля phones конкурента.
|
|
[$t, $u, $run] = dupSetup();
|
|
$a = dupProp($t, $run, 'AutoMoney', ['79135396546']);
|
|
$b = dupProp($t, $run, 'Автодом24', ['79135396546', '73912000000']);
|
|
dupProp($t, $run, 'Другая', ['79990001122']); // свой номер — не в группе
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')->assertOk();
|
|
|
|
$resp->assertJsonCount(1, 'groups');
|
|
// причина = именно СВЯЗАВШИЙ номер, а не весь список телефонов
|
|
expect($resp->json('groups.0.reason'))->toContain('79135396546')
|
|
->and($resp->json('groups.0.reason'))->not->toContain('73912000000');
|
|
$ids = collect($resp->json('groups.0.competitors'))->pluck('id')->sort()->values()->all();
|
|
expect($ids)->toBe(collect([$a->id, $b->id])->sort()->values()->all());
|
|
});
|
|
|
|
it('группа несёт сайт и ссылки-справочники каждой фирмы — чтобы клиент мог проверить и решить', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$a = AutopodborCompetitor::create([
|
|
'tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Сфера займа', 'dedup_key' => 'p:sf',
|
|
'box' => 'proposal', 'phones' => ['73912329282'],
|
|
'site_url' => 'sfera.ru', 'directory_urls' => ['https://2gis.ru/krasnoyarsk/firm/1'],
|
|
]);
|
|
$b = AutopodborCompetitor::create([
|
|
'tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Ломбард Экспресс', 'dedup_key' => 'p:le',
|
|
'box' => 'proposal', 'phones' => ['73912329282'],
|
|
'site_url' => null, 'directory_urls' => ['https://yandex.ru/maps/org/le/2'],
|
|
]);
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')->assertOk();
|
|
|
|
$comp = collect($resp->json('groups.0.competitors'))->keyBy('name');
|
|
expect($comp['Сфера займа']['site_url'])->toBe('sfera.ru')
|
|
->and($comp['Сфера займа']['directory_urls'])->toContain('https://2gis.ru/krasnoyarsk/firm/1')
|
|
->and($comp['Ломбард Экспресс']['site_url'])->toBeNull()
|
|
->and($comp['Ломбард Экспресс']['directory_urls'])->toContain('https://yandex.ru/maps/org/le/2');
|
|
});
|
|
|
|
it('ответ помечает ВЫЖИВАЮЩУЮ карточку — и это НЕ обязательно первая по номеру', function () {
|
|
// Баг «САПС/Метрополис»: клиент переименовал свежую карточку и слил — а выжила ДРУГАЯ (с проектами
|
|
// /в поле), его имя затёрлось. Чтобы этого не было вслепую, ответ помечает, какая карточка останется,
|
|
// а фронт по ней ставит имя по умолчанию и показывает «останется эта».
|
|
[$t, $u, $run] = dupSetup();
|
|
$prop = dupProp($t, $run, 'Метрополис', ['73912555555']); // свежее предложение, меньший id, без источников
|
|
$field = dupComp($t, $run, 'СМ-сити'); // полевая, больший id, с источником в поле
|
|
dupCall($t, $run, $field, '73912555555'); // общий телефон → одна группа
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?scope=cross')->assertOk();
|
|
|
|
$comp = collect($resp->json('groups.0.competitors'))->keyBy('id');
|
|
expect($comp[$field->id]['survivor'])->toBeTrue() // выживает полевая (в ней работа), хоть id больше
|
|
->and($comp[$prop->id]['survivor'])->toBeFalse();
|
|
// ровно одна выжившая на группу
|
|
expect(collect($resp->json('groups.0.competitors'))->where('survivor', true))->toHaveCount(1);
|
|
});
|
|
|
|
it('предложения: единый номер застройщика у многих проектов ГРУППИРУЕТ (порог людного номера снят)', function () {
|
|
// Кейс «Образцово» (реш. владельца 05.07): у застройщика (СМ.Сити) один номер отдела продаж
|
|
// висит на всех его ЖК. Прежний порог 4 прятал их как «рекламный номер». Теперь показываем
|
|
// группу — склейку клиент подтверждает вручную.
|
|
[$t, $u, $run] = dupSetup();
|
|
for ($i = 1; $i <= 9; $i++) {
|
|
dupProp($t, $run, 'ЖК '.$i, ['73912779901']); // единый номер застройщика на 9 проектах
|
|
}
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')
|
|
->assertOk()
|
|
->assertJsonCount(1, 'groups');
|
|
expect($resp->json('groups.0.competitors'))->toHaveCount(9);
|
|
expect(mb_strtolower($resp->json('groups.0.reason')))->toContain('телефон');
|
|
});
|
|
|
|
it('GET field/duplicate-groups — группирует по общему адресу источников (слабый сигнал)', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$a = dupComp($t, $run, 'ГорЗайм');
|
|
$b = dupComp($t, $run, 'Центр Займов');
|
|
// Разные телефоны, но ОДИН адрес → должно предложить объединение по адресу.
|
|
foreach ([[$a, '79001112233'], [$b, '79004445566']] as [$comp, $phone]) {
|
|
AutopodborSource::create([
|
|
'tenant_id' => $t->id, 'competitor_id' => $comp->id, 'study_run_id' => $run->id,
|
|
'signal_type' => 'call', 'identifier' => $phone, 'dedup_key' => 'call:'.$phone,
|
|
'box' => 'field', 'confirmations' => 1, 'office' => 'Водопьянова 2',
|
|
]);
|
|
}
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')->assertOk();
|
|
|
|
$resp->assertJsonCount(1, 'groups');
|
|
expect(mb_strtolower($resp->json('groups.0.reason')))->toContain('адрес');
|
|
$ids = collect($resp->json('groups.0.competitors'))->pluck('id')->sort()->values()->all();
|
|
expect($ids)->toBe(collect([$a->id, $b->id])->sort()->values()->all());
|
|
});
|
|
|
|
it('GET field/duplicate-groups — дублей нет → пустой список групп', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$a = dupComp($t, $run, 'A');
|
|
$b = dupComp($t, $run, 'B');
|
|
dupCall($t, $run, $a, '79135396546');
|
|
dupCall($t, $run, $b, '79990001122');
|
|
|
|
$this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')
|
|
->assertOk()
|
|
->assertJsonCount(0, 'groups');
|
|
});
|
|
|
|
it('GET field/duplicate-groups?scope=cross — ловит предложение как дубль карточки поля', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$f = dupComp($t, $run, 'СМ-сити'); // в поле
|
|
$p = AutopodborCompetitor::create([
|
|
'tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Южный берег',
|
|
'dedup_key' => 'p:yb', 'box' => 'proposal',
|
|
]);
|
|
dupCall($t, $run, $f, '73912179941');
|
|
dupCall($t, $run, $p, '73912179941'); // общий телефон — связывает предложение с полем
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?scope=cross')->assertOk();
|
|
|
|
$resp->assertJsonCount(1, 'groups');
|
|
$comp = collect($resp->json('groups.0.competitors'))->keyBy('id');
|
|
expect($comp->keys()->map(fn ($k) => (int) $k)->sort()->values()->all())
|
|
->toBe(collect([$f->id, $p->id])->sort()->values()->all());
|
|
// в выдаче видно, кто из какого ящика (чтобы клиент понял: это предложение к карточке поля)
|
|
expect($comp[$p->id]['box'])->toBe('proposal')
|
|
->and($comp[$f->id]['box'])->toBe('field');
|
|
});
|
|
|
|
it('scope=cross — пара чисто-предложений (без карточки поля) НЕ показывается на экране поля', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
// Два предложения с общим телефоном — но НИ ОДНОГО в поле → на экране поля не показываем.
|
|
$p1 = AutopodborCompetitor::create(['tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Предл A', 'dedup_key' => 'p:a', 'box' => 'proposal']);
|
|
$p2 = AutopodborCompetitor::create(['tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Предл B', 'dedup_key' => 'p:b', 'box' => 'proposal']);
|
|
dupCall($t, $run, $p1, '73912179941');
|
|
dupCall($t, $run, $p2, '73912179941');
|
|
|
|
$this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?scope=cross')
|
|
->assertOk()
|
|
->assertJsonCount(0, 'groups');
|
|
});
|
|
|
|
it('GET field/duplicate-groups БЕЗ scope — по-прежнему один ящик (поле), предложения не мешают', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$f = dupComp($t, $run, 'Поле A');
|
|
$p = AutopodborCompetitor::create([
|
|
'tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Предл A',
|
|
'dedup_key' => 'p:a', 'box' => 'proposal',
|
|
]);
|
|
dupCall($t, $run, $f, '73912179941');
|
|
dupCall($t, $run, $p, '73912179941');
|
|
|
|
// без scope=cross поле и предложение НЕ группируются (обратная совместимость)
|
|
$this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')
|
|
->assertOk()
|
|
->assertJsonCount(0, 'groups');
|
|
});
|
|
|
|
it('POST field/merge — сливает подтверждённую группу, возвращает выжившего, дубль исчезает', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$a = dupComp($t, $run, 'A');
|
|
$b = dupComp($t, $run, 'B');
|
|
dupCall($t, $run, $a, '79135396546');
|
|
dupCall($t, $run, $b, '79135396546');
|
|
|
|
$resp = $this->actingAs($u)->postJson('/api/autopodbor/field/merge', [
|
|
'competitor_ids' => [$a->id, $b->id],
|
|
])->assertOk();
|
|
|
|
$survivor = $resp->json('survivor_id');
|
|
expect(AutopodborCompetitor::whereIn('id', [$a->id, $b->id])->count())->toBe(1);
|
|
expect($survivor)->toBeIn([$a->id, $b->id]);
|
|
});
|
|
|
|
it('POST field/merge — меньше двух id → 422', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$a = dupComp($t, $run, 'A');
|
|
|
|
$this->actingAs($u)->postJson('/api/autopodbor/field/merge', [
|
|
'competitor_ids' => [$a->id],
|
|
])->assertStatus(422);
|
|
});
|
|
|
|
it('merge-events: после склейки в журнале видно «что во что слили», и карточку можно ВЕРНУТЬ', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$keep = dupComp($t, $run, 'СМ-сити'); // выживет (источник в поле)
|
|
$gone = dupProp($t, $run, 'Метрополис', ['79135396546']); // поглотится
|
|
dupCall($t, $run, $keep, '79135396546');
|
|
|
|
// Сливаем (кросс-ящик поле+предложение)
|
|
$this->actingAs($u)->postJson('/api/autopodbor/field/merge', [
|
|
'competitor_ids' => [$keep->id, $gone->id],
|
|
])->assertOk();
|
|
|
|
// Журнал показывает слияние: поглощена «Метрополис», выжившая — «СМ-сити»
|
|
$events = $this->actingAs($u)->getJson('/api/autopodbor/field/merge-events')->assertOk();
|
|
$events->assertJsonCount(1, 'events');
|
|
expect($events->json('events.0.survivor_name'))->toBe('СМ-сити');
|
|
expect(collect($events->json('events.0.absorbed'))->pluck('name')->all())->toContain('Метрополис');
|
|
$eventId = $events->json('events.0.id');
|
|
|
|
// Возврат: «Метрополис» появляется снова как отдельная карточка-предложение
|
|
$restore = $this->actingAs($u)->postJson("/api/autopodbor/field/merge-events/{$eventId}/restore")->assertOk();
|
|
expect(collect($restore->json('restored'))->pluck('name')->all())->toContain('Метрополис');
|
|
expect(AutopodborCompetitor::where('tenant_id', $t->id)->where('name', 'Метрополис')->where('box', 'proposal')->exists())->toBeTrue();
|
|
|
|
// Повторный возврат из того же события — 422 (уже вернули)
|
|
$this->actingAs($u)->postJson("/api/autopodbor/field/merge-events/{$eventId}/restore")->assertStatus(422);
|
|
});
|
|
|
|
it('откат склейки: поглощённая карточка имела ПРОЕКТ — после «Вернуть» проект снова у воскрешённой карточки, а не у выжившей', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
|
|
// Обе карточки владеют своим проектом (иначе побеждает та, у кого проектов больше —
|
|
// ей и достанется выживание). Тай-брейк — по возрасту (меньший id), поэтому $keep создаём первым.
|
|
$keep = dupComp($t, $run, 'СМ-сити');
|
|
$keepProject = Project::factory()->create(['tenant_id' => $t->id, 'name' => 'СМ-сити', 'is_active' => true, 'preflight_blocked_at' => null]);
|
|
AutopodborSource::create([
|
|
'tenant_id' => $t->id, 'competitor_id' => $keep->id, 'study_run_id' => $run->id,
|
|
'signal_type' => 'call', 'identifier' => '79001112233', 'dedup_key' => 'call:79001112233',
|
|
'box' => 'field', 'confirmations' => 1, 'created_project_id' => $keepProject->id,
|
|
]);
|
|
|
|
$gone = dupComp($t, $run, 'Метрополис');
|
|
$goneProject = Project::factory()->create(['tenant_id' => $t->id, 'name' => 'Метрополис', 'is_active' => true, 'preflight_blocked_at' => null]);
|
|
$goneSource = AutopodborSource::create([
|
|
'tenant_id' => $t->id, 'competitor_id' => $gone->id, 'study_run_id' => $run->id,
|
|
'signal_type' => 'call', 'identifier' => '79135396546', 'dedup_key' => 'call:79135396546',
|
|
'box' => 'field', 'confirmations' => 1, 'created_project_id' => $goneProject->id,
|
|
]);
|
|
|
|
$this->actingAs($u)->postJson('/api/autopodbor/field/merge', [
|
|
'competitor_ids' => [$keep->id, $gone->id],
|
|
])->assertOk();
|
|
|
|
// До отката: проект поглощённой уже переехал (и переименовался) под выжившего — это ожидаемое
|
|
// поведение слияния, баг именно в том, что откат его назад не возвращает.
|
|
expect(AutopodborSource::find($goneSource->id)->competitor_id)->toBe($keep->id);
|
|
|
|
$events = $this->actingAs($u)->getJson('/api/autopodbor/field/merge-events')->assertOk();
|
|
$eventId = $events->json('events.0.id');
|
|
|
|
$restore = $this->actingAs($u)->postJson("/api/autopodbor/field/merge-events/{$eventId}/restore")->assertOk();
|
|
$restoredId = collect($restore->json('restored'))->firstWhere('name', 'Метрополис')['id'];
|
|
|
|
// Проект снова принадлежит воскрешённой карточке (через источник), не выжившей.
|
|
expect(AutopodborSource::where('created_project_id', $goneProject->id)->where('competitor_id', $restoredId)->exists())->toBeTrue();
|
|
expect(AutopodborSource::where('competitor_id', $keep->id)->where('created_project_id', $goneProject->id)->exists())->toBeFalse();
|
|
// Имя проекта вернулось к прежнему (было переименовано слиянием).
|
|
expect($goneProject->fresh()->name)->toBe('Метрополис');
|
|
});
|
|
|
|
it('откат склейки: поглощённая карточка имела ИСТОЧНИКИ (без проекта) — после «Вернуть» они снова у неё', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$keep = dupComp($t, $run, 'A');
|
|
dupCall($t, $run, $keep, '79001112233');
|
|
$gone = dupComp($t, $run, 'B');
|
|
dupCall($t, $run, $gone, '79004445566');
|
|
|
|
$this->actingAs($u)->postJson('/api/autopodbor/field/merge', [
|
|
'competitor_ids' => [$keep->id, $gone->id],
|
|
])->assertOk();
|
|
|
|
$events = $this->actingAs($u)->getJson('/api/autopodbor/field/merge-events')->assertOk();
|
|
$eventId = $events->json('events.0.id');
|
|
$restore = $this->actingAs($u)->postJson("/api/autopodbor/field/merge-events/{$eventId}/restore")->assertOk();
|
|
$restoredId = collect($restore->json('restored'))->firstWhere('name', 'B')['id'];
|
|
|
|
expect(AutopodborSource::where('dedup_key', 'call:79004445566')->where('competitor_id', $restoredId)->exists())->toBeTrue();
|
|
expect(AutopodborSource::where('competitor_id', $keep->id)->where('dedup_key', 'call:79004445566')->exists())->toBeFalse();
|
|
});
|
|
|
|
it('журнал БЕЗ снимка источников (старая запись до миграции) — откат честно возвращает только карточку и помечает частичный откат', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$survivor = dupComp($t, $run, 'Survivor');
|
|
$ev = AutopodborMergeEvent::create([
|
|
'tenant_id' => $t->id,
|
|
'user_id' => $u->id,
|
|
'survivor_id' => $survivor->id,
|
|
'survivor_name' => $survivor->name,
|
|
'absorbed' => [[
|
|
'id' => 999999, 'name' => 'Старый Поглощённый', 'site_url' => null, 'description' => null,
|
|
'is_federal' => false, 'box' => 'field', 'directory_urls' => [], 'phones' => [],
|
|
]],
|
|
// absorbed_sources НЕ передан — эмулируем запись журнала, созданную ДО этой миграции.
|
|
]);
|
|
|
|
$restore = $this->actingAs($u)->postJson("/api/autopodbor/field/merge-events/{$ev->id}/restore")->assertOk();
|
|
|
|
expect(collect($restore->json('restored'))->pluck('name')->all())->toContain('Старый Поглощённый');
|
|
expect($restore->json('partial'))->toBeTrue();
|
|
expect(AutopodborCompetitor::where('tenant_id', $t->id)->where('name', 'Старый Поглощённый')->exists())->toBeTrue();
|
|
});
|