From 150947c5fbd83ced077506e0a4e136ceca83bf69 Mon Sep 17 00:00:00 2001 From: nns Date: Fri, 9 Oct 2026 20:21:03 +0500 Subject: [PATCH] =?UTF-8?q?fix(migration):=20=D0=B8=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=D0=B8?= =?UTF-8?q?=D1=82=20=D0=B4=D1=83=D0=B1=D0=BB=D1=8C-=D0=A8=D0=9A=20=D0=B8?= =?UTF-8?q?=20=D0=B4=D1=83=D0=B1=D0=BB=D1=8C-=D0=B0=D1=80=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D1=83=D0=BB=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA?= =?UTF-8?q?=D0=B8=20(=D0=B4=D0=B8=D0=B7=D0=B0=D0=BC=D0=B1=D0=B8=D0=B3=20?= =?UTF-8?q?=D0=B0=D1=80=D1=82=D0=B8=D0=BA=D1=83=D0=BB=D0=B0,=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D1=8B=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80-=D0=A8?= =?UTF-8?q?=D0=9A)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create: артикул-коллизия → суффикс msId (перенести карточку, не падать на unique) - create/update: ШК не уникален → добавляем код каждой карточке (пары товар|код против повторов), дубли ловит «Проверка данных» Co-Authored-By: Claude Opus 5.5 --- .../MoySklad/MoySkladImportService.cs | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs index ab224d9..368607a 100644 --- a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs +++ b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs @@ -317,11 +317,21 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct)) .ToDictionaryAsync(p => p.Article!, p => p, StringComparer.OrdinalIgnoreCase, ct); // Barcode → Product id (ограничиваем tenant'ом). Нужен для merge-логики. - var existingProductIdByBarcode = await _db.ProductBarcodes + // Код НЕ уникален (Phase44), поэтому при коллизии оставляем ПЕРВОГО владельца + // (для merge-by-barcode legacy). Для «одна карточка — свой ШК» используем + // ownBarcodePairs ниже. + var existingProductIdByBarcode = (await _db.ProductBarcodes .Where(b => b.OrganizationId == orgId) - .ToDictionaryAsync(b => b.Code, b => b.ProductId, ct); + .Select(b => new { b.Code, b.ProductId }).ToListAsync(ct)) + .GroupBy(x => x.Code).ToDictionary(g => g.Key, g => g.First().ProductId); var existingBarcodeSet = new HashSet(existingProductIdByBarcode.Keys); + // Пары (товар, код) — чтобы один и тот же код можно было добавить РАЗНЫМ товарам, + // но не дублировать один код у одного товара при повторных прогонах. + var ownBarcodePairs = new HashSet<(Guid, string)>( + (await _db.ProductBarcodes.Where(b => b.OrganizationId == orgId) + .Select(b => new { b.ProductId, b.Code }).ToListAsync(ct)) + .Select(x => (x.ProductId, x.Code))); // Дедуп внутри входа: один и тот же barcode встречается у нескольких // товаров MoySklad → импортируем первый, остальные пропускаем в отчёт. @@ -425,14 +435,17 @@ await foreach (var p in _client.StreamProductsAsync(token, ct, includeArchived: if (string.IsNullOrEmpty(existing.MoySkladExternalId)) existing.MoySkladExternalId = p.Id; - // Добавляем новые штрихкоды, оставляя уже существующие как есть. + // Добавляем ШК карточки, если у ЭТОГО товара его ещё нет (дубль кода у + // РАЗНЫХ товаров допустим — Phase44). Так баркод-дубль карточки получает + // свой штрихкод, а повторный прогон не плодит дубли-строки. foreach (var b in barcodes) { - if (existingBarcodeSet.Contains(b.Code)) continue; + if (ownBarcodePairs.Contains((existing.Id, b.Code))) continue; b.OrganizationId = orgId; existing.Barcodes.Add(b); existingBarcodeSet.Add(b.Code); - existingProductIdByBarcode[b.Code] = existing.Id; + ownBarcodePairs.Add((existing.Id, b.Code)); + existingProductIdByBarcode.TryAdd(b.Code, existing.Id); } // Прайс retail: если у существующего товара нет цены этого типа — @@ -468,11 +481,17 @@ await foreach (var p in _client.StreamProductsAsync(token, ct, includeArchived: } else { + // Артикул уникален в орг (индекс). Если он уже занят ДРУГОЙ карточкой — + // дизамбигуируем суффиксом msId, чтобы перенести карточку отдельно + // (а не падать на уникальности и не схлопывать её). + var uniqueArticle = article; + if (!string.IsNullOrWhiteSpace(uniqueArticle) && existingByArticle.ContainsKey(uniqueArticle)) + uniqueArticle = Trim(article, 480) + "·" + (p.Id ?? Guid.NewGuid().ToString("N"))[..8]; var product = new Product { OrganizationId = orgId, Name = Trim(p.Name, 500) ?? "(без имени)", - Article = Trim(article, 500), + Article = Trim(uniqueArticle, 500), Description = p.Description, UnitOfMeasureId = uomId, Vat = vat, @@ -497,14 +516,10 @@ await foreach (var p in _client.StreamProductsAsync(token, ct, includeArchived: }); } + // ШК НЕ уникален (Phase44): переносим штрихкод КАЖДОЙ карточки, даже если + // такой код уже у другого товара (дубль ловит «Проверка данных» → dup-barcode). foreach (var b in barcodes) { - if (existingBarcodeSet.Contains(b.Code)) - { - errors.Add($"{p.Name}: штрихкод {b.Code} уже занят, пропущен."); - if (progress is not null) progress.Errors = errors; - continue; - } b.OrganizationId = orgId; product.Barcodes.Add(b); existingBarcodeSet.Add(b.Code); @@ -512,7 +527,7 @@ await foreach (var p in _client.StreamProductsAsync(token, ct, includeArchived: _db.Products.Add(product); if (p.Id is not null) existingByMsId[p.Id] = product; - if (!string.IsNullOrWhiteSpace(article)) existingByArticle[article] = product; + if (!string.IsNullOrWhiteSpace(product.Article)) existingByArticle[product.Article!] = product; foreach (var b in product.Barcodes) existingProductIdByBarcode[b.Code] = product.Id; created++; if (progress is not null) progress.Created = created;