diff --git a/src/food-market.api/Controllers/Catalog/ProductsController.cs b/src/food-market.api/Controllers/Catalog/ProductsController.cs index ce32ae5..8f8bfbf 100644 --- a/src/food-market.api/Controllers/Catalog/ProductsController.cs +++ b/src/food-market.api/Controllers/Catalog/ProductsController.cs @@ -213,21 +213,50 @@ public async Task Update(Guid id, [FromBody] ProductInput input, if (e is null) return NotFound(); var allowFractional = await AllowFractionalAsync(ct); - var existingVat = e.Vat; Apply(e, input); e.PurchasePrice = RoundIfNeeded(e.PurchasePrice, allowFractional); - // Если UI не передал Vat (скрыт) — сохраняем что было, не обнуляем. - if (input.Vat is null) e.Vat = existingVat; - _db.ProductBarcodes.RemoveRange(e.Barcodes); - e.Barcodes.Clear(); - foreach (var b in input.Barcodes ?? []) - e.Barcodes.Add(new ProductBarcode { Code = b.Code, Type = b.Type, IsPrimary = b.IsPrimary }); + // Merge barcodes по Code: одинаковые — обновляем поля, лишние удаляем, + // новые добавляем. Это позволяет избежать массового DELETE+INSERT, на + // котором EF может выдать DbUpdateConcurrencyException, если какой-то + // child был удалён параллельно из БД. + var inputBarcodes = (input.Barcodes ?? []).ToList(); + var byCode = e.Barcodes.ToDictionary(b => b.Code, b => b); + var inputCodes = inputBarcodes.Select(b => b.Code).ToHashSet(); + foreach (var existing in e.Barcodes.ToList()) + if (!inputCodes.Contains(existing.Code)) _db.ProductBarcodes.Remove(existing); + foreach (var b in inputBarcodes) + { + if (byCode.TryGetValue(b.Code, out var ex)) + { + ex.Type = b.Type; + ex.IsPrimary = b.IsPrimary; + } + else + { + e.Barcodes.Add(new ProductBarcode { Code = b.Code, Type = b.Type, IsPrimary = b.IsPrimary }); + } + } - _db.ProductPrices.RemoveRange(e.Prices); - e.Prices.Clear(); - foreach (var pr in input.Prices ?? []) - e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = RoundIfNeeded(pr.Amount, allowFractional), CurrencyId = pr.CurrencyId }); + // Merge prices по PriceTypeId. + var inputPrices = (input.Prices ?? []).ToList(); + var byPriceType = e.Prices.ToDictionary(p => p.PriceTypeId, p => p); + var inputPriceTypes = inputPrices.Select(p => p.PriceTypeId).ToHashSet(); + foreach (var existing in e.Prices.ToList()) + if (!inputPriceTypes.Contains(existing.PriceTypeId)) _db.ProductPrices.Remove(existing); + foreach (var pr in inputPrices) + { + var amount = RoundIfNeeded(pr.Amount, allowFractional); + if (byPriceType.TryGetValue(pr.PriceTypeId, out var ex)) + { + ex.Amount = amount; + ex.CurrencyId = pr.CurrencyId; + } + else + { + e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = amount, CurrencyId = pr.CurrencyId }); + } + } try { @@ -237,6 +266,10 @@ public async Task Update(Guid id, [FromBody] ProductInput input, { return BadRequest(new { error = $"Артикул «{e.Article}» уже занят в этой организации." }); } + catch (DbUpdateConcurrencyException) + { + return Conflict(new { error = "Товар изменён в другом окне или сессии. Перезагрузите страницу и попробуйте снова." }); + } return NoContent(); }