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;