fix(catalog): FK-guard удаления контрагента + валидация полей товара
Найдено в catalog-edge: - DELETE контрагента, на которого ссылаются supplies/retail-sales/products (DefaultSupplier), отдавал 500 (DbUpdateException 23503) вместо понятного 409. Добавлен явный чек использования → Conflict со списком где занят. - POST товара с пустым Name проходил до FK-проверки и падал неинформативно; теперь явный 400 с указанием поля. На ProductInput навешены [Required]/[MinLength]/[StringLength] на Name/Article/ImageUrl — отсекаем пустые и сверхдлинные значения на уровне модели. catalog-edge: 12/12. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
32729e72a3
commit
e13ac655e5
|
|
@ -103,6 +103,23 @@ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
|||
{
|
||||
var e = await _db.Counterparties.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
|
||||
// FK-guard: counterparty используется как Supplier в supplies / customer
|
||||
// в retail-sales / default supplier у products. Без явного чека EF
|
||||
// отдаёт 500 «DbUpdateException 23503 violates foreign key constraint»
|
||||
// вместо понятного 409 — пользователь не понимает что чинить.
|
||||
var usedAsSupplier = await _db.Supplies.AnyAsync(s => s.SupplierId == id, ct);
|
||||
var usedAsCustomer = await _db.RetailSales.AnyAsync(s => s.CustomerId == id, ct);
|
||||
var usedAsDefault = await _db.Products.AnyAsync(p => p.DefaultSupplierId == id, ct);
|
||||
if (usedAsSupplier || usedAsCustomer || usedAsDefault)
|
||||
{
|
||||
return Conflict(new
|
||||
{
|
||||
error = "Нельзя удалить контрагента: он используется в документах или товарах.",
|
||||
usedAsSupplier, usedAsCustomer, usedAsDefault,
|
||||
});
|
||||
}
|
||||
|
||||
_db.Counterparties.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
|
|
|
|||
|
|
@ -198,6 +198,8 @@ public async Task<ActionResult<ProductDto>> Get(Guid id, CancellationToken ct)
|
|||
[HttpPost, Authorize(Roles = "Admin,Storekeeper")]
|
||||
public async Task<ActionResult<ProductDto>> Create([FromBody] ProductInput input, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.Name))
|
||||
return BadRequest(new { error = "Название товара обязательно.", field = nameof(input.Name) });
|
||||
if (RequiredGuid.FirstMissing(
|
||||
(nameof(input.UnitOfMeasureId), input.UnitOfMeasureId),
|
||||
(nameof(input.ProductGroupId), input.ProductGroupId)) is { } missingFk)
|
||||
|
|
|
|||
|
|
@ -85,12 +85,14 @@ public record CounterpartyInput(
|
|||
public record ProductBarcodeInput(string Code, BarcodeType Type = BarcodeType.Ean13, bool IsPrimary = false);
|
||||
public record ProductPriceInput(Guid PriceTypeId, [Range(0, 1e10)] decimal Amount, Guid CurrencyId);
|
||||
public record ProductInput(
|
||||
string Name, string? Article, string? Description,
|
||||
[Required, MinLength(1), StringLength(500)] string Name,
|
||||
[StringLength(500)] string? Article,
|
||||
string? Description,
|
||||
Guid UnitOfMeasureId, [Range(0, 100)] decimal? Vat, bool VatEnabled,
|
||||
Guid ProductGroupId, Guid? DefaultSupplierId, Guid? CountryOfOriginId,
|
||||
bool IsService = false, Packaging Packaging = Packaging.Piece, bool IsMarked = false,
|
||||
[Range(0, 1e10)] decimal? MinStock = null, [Range(0, 1e10)] decimal? MaxStock = null,
|
||||
[Range(0, 1e10)] decimal? ReferencePrice = null, Guid? PurchaseCurrencyId = null,
|
||||
string? ImageUrl = null,
|
||||
[StringLength(1000)] string? ImageUrl = null,
|
||||
IReadOnlyList<ProductPriceInput>? Prices = null,
|
||||
IReadOnlyList<ProductBarcodeInput>? Barcodes = null);
|
||||
|
|
|
|||
Loading…
Reference in a new issue