diff --git a/src/food-market.api/Controllers/Catalog/ProductsController.cs b/src/food-market.api/Controllers/Catalog/ProductsController.cs index b07a341..cf9d836 100644 --- a/src/food-market.api/Controllers/Catalog/ProductsController.cs +++ b/src/food-market.api/Controllers/Catalog/ProductsController.cs @@ -23,6 +23,23 @@ public ProductsController(AppDbContext db, ITenantContext tenant) _tenant = tenant; } + // Следующий числовой артикул для организации. Находит max(Article::int) + // среди артикулов, которые полностью состоят из цифр, и прибавляет 1. + // Если числовых артикулов нет — возвращает "1". + private async Task GenerateNextArticleAsync(CancellationToken ct) + { + var articles = await _db.Products + .Where(p => p.Article != null && p.Article != "") + .Select(p => p.Article!) + .ToListAsync(ct); + var next = 1; + foreach (var a in articles) + { + if (int.TryParse(a, out var n) && n >= next) next = n + 1; + } + return next.ToString(); + } + // Дефолт Vat для нового товара — из страны организации (Country.VatRate). private async Task ResolveDefaultVatAsync(CancellationToken ct) { @@ -124,6 +141,8 @@ public async Task> Create([FromBody] ProductInput input Apply(e, input); // Если UI скрывает поле Vat (showVatEnabledOnProduct=false) и прислал null — дефолт из страны. if (input.Vat is null) e.Vat = await ResolveDefaultVatAsync(ct); + // Авто-артикул: если пользователь не указал — генерируем числовой. + if (string.IsNullOrWhiteSpace(e.Article)) e.Article = await GenerateNextArticleAsync(ct); foreach (var b in input.Barcodes ?? []) e.Barcodes.Add(new ProductBarcode { Code = b.Code, Type = b.Type, IsPrimary = b.IsPrimary }); @@ -131,7 +150,14 @@ public async Task> Create([FromBody] ProductInput input e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = pr.Amount, CurrencyId = pr.CurrencyId }); _db.Products.Add(e); - await _db.SaveChangesAsync(ct); + try + { + await _db.SaveChangesAsync(ct); + } + catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("IX_products_OrganizationId_Article") == true) + { + return BadRequest(new { error = $"Артикул «{e.Article}» уже занят в этой организации." }); + } var dto = await GetInternalAsync(e.Id, ct); return CreatedAtAction(nameof(Get), new { id = e.Id }, dto); } @@ -160,7 +186,14 @@ public async Task Update(Guid id, [FromBody] ProductInput input, foreach (var pr in input.Prices ?? []) e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = pr.Amount, CurrencyId = pr.CurrencyId }); - await _db.SaveChangesAsync(ct); + try + { + await _db.SaveChangesAsync(ct); + } + catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("IX_products_OrganizationId_Article") == true) + { + return BadRequest(new { error = $"Артикул «{e.Article}» уже занят в этой организации." }); + } return NoContent(); }