ADD EV3-357 a döntési lap adja a csoportosítást (csoportokon átívelő összevonás)
A megrendelő 2026-08-27-én hat esetben jelezte, hogy két külön névcsoportunk valójában ugyanaz a cég: a rövid név és a cégformás név (Kőröstej / Kőröstej Kft, Pick / Pick Szeged Zrt, White Lake / White Lake Kft, ...). A névnormalizálás ezt nem tudhatja, mert a nevek érdemben különböznek. - buildPlan a LAP "Csoport" oszlopa szerint csoportosít, nem az adatbázis normalizálása szerint. Így a lap szélesebb csoportot is kijelölhet - a végrehajtásba nem kell csoportokon átívelő logika. - validateGrouping: a lap szélesebb csoportot csinálhat, SZŰKEBBET nem. Ha az azonos nevű rekordok külön csoportba kerülnének, a végén két azonos nevű gyártó maradna, vagyis pont a duplikációt állítanánk elő. Ilyenkor a csoport kimarad a végrehajtásból, nem csak figyelmeztetés. - a megtartott REKORD továbbra is a legtöbb terméket tartalmazó (a legkevesebb sor mozdul), a NEVE viszont a választott cégnév 2 új teszt a csoportokon átívelő összevonásra és a szétszórás elutasítására. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2ced04c086
commit
7e32a870ae
@ -99,72 +99,99 @@ private function referenceCounts(): array
|
||||
/**
|
||||
* Összevonási terv a döntési lap sorai alapján.
|
||||
*
|
||||
* A csoportosítás forrása a LAP "Csoport" oszlopa, nem az adatbázis normalizálása.
|
||||
* Ez azért fontos, mert a megrendelő olyan gyártókat is egybe akarhat vonni, amiket a
|
||||
* névnormalizálás nem tud összekötni: a "Kőröstej" és a "Kőröstej Kft" ugyanaz a cég,
|
||||
* de a nevük érdemben különbözik. Ha a lap ezeket egy csoportba teszi, itt is egy
|
||||
* összevonás lesz belőlük - így nem kell a végrehajtásba csoportokon átívelő logika.
|
||||
*
|
||||
* @param array<int, array{group: int|string, id: int, final_name: ?string}> $decisions
|
||||
* @return array{merges: array<int, array>, errors: array<int, string>}
|
||||
*/
|
||||
public function buildPlan(array $decisions): array
|
||||
{
|
||||
$groups = $this->duplicateGroups();
|
||||
$byId = [];
|
||||
|
||||
$normalizedById = [];
|
||||
$memberById = [];
|
||||
foreach ($groups as $normalized => $members) {
|
||||
foreach ($members as $member) {
|
||||
$byId[$member['producer']->id] = $normalized;
|
||||
$normalizedById[$member['producer']->id] = $normalized;
|
||||
$memberById[$member['producer']->id] = $member;
|
||||
}
|
||||
}
|
||||
|
||||
$rowsByGroup = [];
|
||||
$sheetGroups = [];
|
||||
$finalNames = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ($decisions as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
|
||||
if (! isset($byId[$id])) {
|
||||
if (! isset($normalizedById[$id])) {
|
||||
$errors[] = "A(z) {$id} azonosítójú gyártó nem szerepel duplikált csoportban "
|
||||
. '(időközben megváltozott az adat, vagy más környezetből származik a lap).';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized = $byId[$id];
|
||||
$rowsByGroup[$normalized][] = $id;
|
||||
$key = trim((string) ($row['group'] ?? ''));
|
||||
|
||||
if ($key === '') {
|
||||
$errors[] = "A(z) {$id} azonosítójú sorból hiányzik a csoport száma.";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$sheetGroups[$key][] = $id;
|
||||
|
||||
$name = trim((string) ($row['final_name'] ?? ''));
|
||||
if ($name !== '') {
|
||||
$finalNames[$normalized] ??= $name;
|
||||
$finalNames[$key] ??= $name;
|
||||
}
|
||||
}
|
||||
|
||||
$validation = $this->validateGrouping($sheetGroups, $normalizedById, $groups);
|
||||
$errors = array_merge($errors, $validation['errors']);
|
||||
|
||||
$merges = [];
|
||||
|
||||
foreach ($rowsByGroup as $normalized => $ids) {
|
||||
$members = $groups[$normalized];
|
||||
$memberIds = array_map(fn ($m) => $m['producer']->id, $members);
|
||||
foreach ($sheetGroups as $key => $ids) {
|
||||
// A hibás csoportot NEM hajtjuk végre: egy hiányzó tag azt jelentené, hogy
|
||||
// az összevonás után is maradna azonos nevű rekord.
|
||||
if (isset($validation['invalid'][$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! isset($finalNames[$normalized])) {
|
||||
$errors[] = 'Hiányzó végleges név ehhez a csoporthoz: '
|
||||
$members = array_values(array_map(fn ($id) => $memberById[$id], array_unique($ids)));
|
||||
|
||||
if (! isset($finalNames[$key])) {
|
||||
$errors[] = 'Hiányzó végleges név a(z) ' . $key . '. csoportnál: '
|
||||
. implode(' / ', array_map(fn ($m) => '"' . $m['producer']->name . '"', $members));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$missing = array_diff($memberIds, $ids);
|
||||
if ($missing !== []) {
|
||||
$errors[] = 'A döntési lap nem tartalmazza a csoport minden tagját (hiányzó azonosító: '
|
||||
. implode(', ', $missing) . '). Generáld újra a lapot ebben a környezetben.';
|
||||
|
||||
continue;
|
||||
}
|
||||
// A megtartott REKORD a legtöbb terméket tartalmazó (így mozdul a legkevesebb
|
||||
// sor), a NEVE viszont a megrendelő által választott végleges név lesz.
|
||||
usort($members, function ($a, $b) {
|
||||
return [$b['counts']['products'], $b['counts']['order_archives_items'], $a['producer']->id]
|
||||
<=> [$a['counts']['products'], $a['counts']['order_archives_items'], $b['producer']->id];
|
||||
});
|
||||
|
||||
$keeper = $members[0]['producer'];
|
||||
$losers = array_slice($members, 1);
|
||||
|
||||
if ($losers === []) {
|
||||
continue; // egytagú csoport: nincs mit összevonni
|
||||
}
|
||||
|
||||
$merges[] = [
|
||||
'normalized' => $normalized,
|
||||
'group' => $key,
|
||||
'keeper_id' => $keeper->id,
|
||||
'keeper_name' => $keeper->name,
|
||||
'final_name' => $finalNames[$normalized],
|
||||
'rename' => trim((string) $keeper->name) !== $finalNames[$normalized],
|
||||
'final_name' => $finalNames[$key],
|
||||
'rename' => trim((string) $keeper->name) !== $finalNames[$key],
|
||||
'from' => array_map(fn ($m) => [
|
||||
'id' => $m['producer']->id,
|
||||
'name' => $m['producer']->name,
|
||||
@ -176,6 +203,57 @@ public function buildPlan(array $decisions): array
|
||||
return ['merges' => $merges, 'errors' => $errors];
|
||||
}
|
||||
|
||||
/**
|
||||
* A lap-csoportosítás ellenőrzése.
|
||||
*
|
||||
* A lap SZÉLESEBB csoportot csinálhat, mint a névnormalizálás (ez a cél), de
|
||||
* SZŰKEBBET nem: ha az azonos nevű rekordok külön lap-csoportba kerülnének, a
|
||||
* végén két azonos nevű gyártó maradna - vagyis pont a duplikációt állítanánk elő.
|
||||
*
|
||||
* @return array{errors: array<int, string>, invalid: array<string, true>}
|
||||
*/
|
||||
private function validateGrouping(array $sheetGroups, array $normalizedById, array $groups): array
|
||||
{
|
||||
$errors = [];
|
||||
$invalid = [];
|
||||
$sheetKeysByNormalized = [];
|
||||
$idsByNormalized = [];
|
||||
|
||||
foreach ($sheetGroups as $key => $ids) {
|
||||
foreach ($ids as $id) {
|
||||
$normalized = $normalizedById[$id];
|
||||
$sheetKeysByNormalized[$normalized][$key] = true;
|
||||
$idsByNormalized[$normalized][$id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($sheetKeysByNormalized as $normalized => $keys) {
|
||||
if (count($keys) > 1) {
|
||||
$errors[] = 'Az azonos nevű gyártók ("' . $normalized . '") több csoportba kerültek a lapon ('
|
||||
. implode(', ', array_keys($keys)) . '). Ezeket egy csoportba kell tenni, '
|
||||
. 'különben duplikátum maradna utánuk.';
|
||||
|
||||
foreach (array_keys($keys) as $key) {
|
||||
$invalid[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$expected = array_map(fn ($m) => $m['producer']->id, $groups[$normalized]);
|
||||
$missing = array_diff($expected, array_keys($idsByNormalized[$normalized]));
|
||||
|
||||
if ($missing !== []) {
|
||||
$errors[] = 'A döntési lap nem tartalmazza a csoport minden tagját (hiányzó azonosító: '
|
||||
. implode(', ', $missing) . '). Generáld újra a lapot ebben a környezetben.';
|
||||
|
||||
foreach (array_keys($keys) as $key) {
|
||||
$invalid[$key] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['errors' => $errors, 'invalid' => $invalid];
|
||||
}
|
||||
|
||||
/**
|
||||
* A terv végrehajtása. Előbb visszaállítási pontot ír, csak utána módosít.
|
||||
*
|
||||
|
||||
@ -98,6 +98,52 @@ function decisionsFor(array $rows): array
|
||||
expect($plan['errors'][0])->toContain('nem szerepel duplikált csoportban');
|
||||
});
|
||||
|
||||
test('a lap egy csoportba tehet olyan gyártókat is, amiket a normalizálás nem köt össze', function () {
|
||||
// A megrendelő döntése: a "Kőröstej" és a "Kőröstej Kft" ugyanaz a cég. A
|
||||
// névnormalizálás ezt nem tudhatja, a lap Csoport oszlopa viszont kifejezi.
|
||||
$short = producer('Kőröstej');
|
||||
$shortDup = producer('Kőröstej ');
|
||||
$long = producer('Kőröstej Kft');
|
||||
$longDup = producer('KŐRÖSTEJ KFT');
|
||||
|
||||
productFor($short, 'A1');
|
||||
productFor($short, 'A2');
|
||||
productFor($long, 'B1');
|
||||
|
||||
$plan = app(ProducerDeduplicator::class)->buildPlan([
|
||||
['group' => 7, 'id' => $short->id, 'final_name' => 'Kőröstej Kft'],
|
||||
['group' => 7, 'id' => $shortDup->id, 'final_name' => null],
|
||||
['group' => 7, 'id' => $long->id, 'final_name' => null],
|
||||
['group' => 7, 'id' => $longDup->id, 'final_name' => null],
|
||||
]);
|
||||
|
||||
expect($plan['errors'])->toBe([])
|
||||
->and($plan['merges'])->toHaveCount(1);
|
||||
|
||||
$merge = $plan['merges'][0];
|
||||
|
||||
// A megtartott REKORD a legtöbb terméket tartalmazó, a NEVE viszont a választott
|
||||
// cégnév - így a legkevesebb sor mozdul, mégis a kívánt név marad.
|
||||
expect($merge['keeper_id'])->toBe($short->id)
|
||||
->and($merge['final_name'])->toBe('Kőröstej Kft')
|
||||
->and($merge['rename'])->toBeTrue()
|
||||
->and($merge['from'])->toHaveCount(3);
|
||||
});
|
||||
|
||||
test('az azonos nevű rekordok szétszórása külön csoportokba hibát ad', function () {
|
||||
// Ez duplikátumot hagyna maga után, ezért nem hajtható végre.
|
||||
$a = producer('Danone');
|
||||
$b = producer('Danone ');
|
||||
|
||||
$plan = app(ProducerDeduplicator::class)->buildPlan([
|
||||
['group' => 1, 'id' => $a->id, 'final_name' => 'Danone'],
|
||||
['group' => 2, 'id' => $b->id, 'final_name' => 'Danone'],
|
||||
]);
|
||||
|
||||
expect($plan['merges'])->toBe([])
|
||||
->and($plan['errors'][0])->toContain('több csoportba kerültek');
|
||||
});
|
||||
|
||||
test('az összevonás átírja a hivatkozásokat és archiválja a beolvasztott rekordot', function () {
|
||||
$keeper = producer('Danone');
|
||||
$loser = producer('Danone ');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user