phase1b: catalog CRUD API (countries, currencies, vat, units, stores, retail points, product groups, counterparties, products)
Application layer: - PagedRequest/PagedResult<T> with sane defaults (pageSize 50, max 500) - CatalogDtos: read DTOs with joined names + input DTOs for upsert - Product input supports nested Prices[] and Barcodes[] for atomic save API controllers (api/catalog/…): - countries, currencies (global, write requires SuperAdmin) - vat-rates, units-of-measure, price-types, stores (write: Admin/Manager) - retail-points (references Store, Admin/Manager write) - product-groups: hierarchy with auto-computed Path, delete guarded against children/products - counterparties: filter by kind (Supplier/Customer/Both), full join with Country - products: includes joined lookups, filter by group/isService/isWeighed/isActive, search by name/article/barcode, write replaces Prices+Barcodes atomically Role semantics: - SuperAdmin: mutates global references only - Admin: mutates/deletes tenant references - Manager: mutates tenant references (no delete on some) - Storekeeper: can manage counterparties and products (but not delete) All endpoints guarded by [Authorize]. Multi-tenant isolation via EF query filter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cb66684134
commit
6b86106937
|
|
@ -0,0 +1,125 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/counterparties")]
|
||||
public class CounterpartiesController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public CounterpartiesController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<CounterpartyDto>>> List(
|
||||
[FromQuery] PagedRequest req,
|
||||
[FromQuery] CounterpartyKind? kind,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var q = _db.Counterparties.Include(c => c.Country).AsNoTracking().AsQueryable();
|
||||
if (kind is not null)
|
||||
{
|
||||
q = q.Where(c => c.Kind == kind || c.Kind == CounterpartyKind.Both);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(c =>
|
||||
c.Name.ToLower().Contains(s) ||
|
||||
(c.LegalName != null && c.LegalName.ToLower().Contains(s)) ||
|
||||
(c.Bin != null && c.Bin.Contains(s)) ||
|
||||
(c.Iin != null && c.Iin.Contains(s)) ||
|
||||
(c.Phone != null && c.Phone.Contains(s)));
|
||||
}
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderBy(c => c.Name)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(c => new CounterpartyDto(
|
||||
c.Id, c.Name, c.LegalName, c.Kind, c.Type,
|
||||
c.Bin, c.Iin, c.TaxNumber, c.CountryId, c.Country != null ? c.Country.Name : null,
|
||||
c.Address, c.Phone, c.Email,
|
||||
c.BankName, c.BankAccount, c.Bik, c.ContactPerson, c.Notes, c.IsActive))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<CounterpartyDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<CounterpartyDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var c = await _db.Counterparties.Include(x => x.Country).AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
return c is null ? NotFound() : new CounterpartyDto(
|
||||
c.Id, c.Name, c.LegalName, c.Kind, c.Type,
|
||||
c.Bin, c.Iin, c.TaxNumber, c.CountryId, c.Country?.Name,
|
||||
c.Address, c.Phone, c.Email,
|
||||
c.BankName, c.BankAccount, c.Bik, c.ContactPerson, c.Notes, c.IsActive);
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")]
|
||||
public async Task<ActionResult<CounterpartyDto>> Create([FromBody] CounterpartyInput input, CancellationToken ct)
|
||||
{
|
||||
var e = Apply(new Counterparty(), input);
|
||||
_db.Counterparties.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id }, await ProjectAsync(e.Id, ct));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager,Storekeeper")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] CounterpartyInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.Counterparties.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
Apply(e, input);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin")]
|
||||
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();
|
||||
_db.Counterparties.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static Counterparty Apply(Counterparty e, CounterpartyInput i)
|
||||
{
|
||||
e.Name = i.Name;
|
||||
e.LegalName = i.LegalName;
|
||||
e.Kind = i.Kind;
|
||||
e.Type = i.Type;
|
||||
e.Bin = i.Bin;
|
||||
e.Iin = i.Iin;
|
||||
e.TaxNumber = i.TaxNumber;
|
||||
e.CountryId = i.CountryId;
|
||||
e.Address = i.Address;
|
||||
e.Phone = i.Phone;
|
||||
e.Email = i.Email;
|
||||
e.BankName = i.BankName;
|
||||
e.BankAccount = i.BankAccount;
|
||||
e.Bik = i.Bik;
|
||||
e.ContactPerson = i.ContactPerson;
|
||||
e.Notes = i.Notes;
|
||||
e.IsActive = i.IsActive;
|
||||
return e;
|
||||
}
|
||||
|
||||
private async Task<CounterpartyDto> ProjectAsync(Guid id, CancellationToken ct)
|
||||
{
|
||||
var c = await _db.Counterparties.Include(x => x.Country).AsNoTracking().FirstAsync(x => x.Id == id, ct);
|
||||
return new CounterpartyDto(
|
||||
c.Id, c.Name, c.LegalName, c.Kind, c.Type,
|
||||
c.Bin, c.Iin, c.TaxNumber, c.CountryId, c.Country?.Name,
|
||||
c.Address, c.Phone, c.Email,
|
||||
c.BankName, c.BankAccount, c.Bik, c.ContactPerson, c.Notes, c.IsActive);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/countries")]
|
||||
public class CountriesController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public CountriesController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<CountryDto>>> List([FromQuery] PagedRequest req, CancellationToken ct)
|
||||
{
|
||||
var q = _db.Countries.AsNoTracking().AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(c => c.Name.ToLower().Contains(s) || c.Code.ToLower().Contains(s));
|
||||
}
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderBy(c => c.SortOrder).ThenBy(c => c.Name)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(c => new CountryDto(c.Id, c.Code, c.Name, c.SortOrder))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<CountryDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<CountryDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var c = await _db.Countries.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
return c is null ? NotFound() : new CountryDto(c.Id, c.Code, c.Name, c.SortOrder);
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "SuperAdmin")]
|
||||
public async Task<ActionResult<CountryDto>> Create([FromBody] CountryInput input, CancellationToken ct)
|
||||
{
|
||||
var e = new Country { Code = input.Code.Trim().ToUpper(), Name = input.Name, SortOrder = input.SortOrder };
|
||||
_db.Countries.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id }, new CountryDto(e.Id, e.Code, e.Name, e.SortOrder));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] CountryInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.Countries.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
e.Code = input.Code.Trim().ToUpper();
|
||||
e.Name = input.Name;
|
||||
e.SortOrder = input.SortOrder;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}"), Authorize(Roles = "SuperAdmin")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.Countries.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
_db.Countries.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/currencies")]
|
||||
public class CurrenciesController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public CurrenciesController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<CurrencyDto>>> List([FromQuery] PagedRequest req, CancellationToken ct)
|
||||
{
|
||||
var q = _db.Currencies.AsNoTracking().AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(c => c.Name.ToLower().Contains(s) || c.Code.ToLower().Contains(s));
|
||||
}
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderBy(c => c.Code)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(c => new CurrencyDto(c.Id, c.Code, c.Name, c.Symbol, c.MinorUnit, c.IsActive))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<CurrencyDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<CurrencyDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var c = await _db.Currencies.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
return c is null ? NotFound() : new CurrencyDto(c.Id, c.Code, c.Name, c.Symbol, c.MinorUnit, c.IsActive);
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "SuperAdmin")]
|
||||
public async Task<ActionResult<CurrencyDto>> Create([FromBody] CurrencyInput input, CancellationToken ct)
|
||||
{
|
||||
var e = new Currency
|
||||
{
|
||||
Code = input.Code.Trim().ToUpper(),
|
||||
Name = input.Name,
|
||||
Symbol = input.Symbol,
|
||||
MinorUnit = input.MinorUnit,
|
||||
IsActive = input.IsActive,
|
||||
};
|
||||
_db.Currencies.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id },
|
||||
new CurrencyDto(e.Id, e.Code, e.Name, e.Symbol, e.MinorUnit, e.IsActive));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] CurrencyInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.Currencies.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
e.Code = input.Code.Trim().ToUpper();
|
||||
e.Name = input.Name;
|
||||
e.Symbol = input.Symbol;
|
||||
e.MinorUnit = input.MinorUnit;
|
||||
e.IsActive = input.IsActive;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/price-types")]
|
||||
public class PriceTypesController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public PriceTypesController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<PriceTypeDto>>> List([FromQuery] PagedRequest req, CancellationToken ct)
|
||||
{
|
||||
var q = _db.PriceTypes.AsNoTracking().AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(p => p.Name.ToLower().Contains(s));
|
||||
}
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderByDescending(p => p.IsDefault).ThenBy(p => p.SortOrder).ThenBy(p => p.Name)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(p => new PriceTypeDto(p.Id, p.Name, p.IsDefault, p.IsRetail, p.SortOrder, p.IsActive))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<PriceTypeDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<PriceTypeDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var p = await _db.PriceTypes.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
return p is null ? NotFound() : new PriceTypeDto(p.Id, p.Name, p.IsDefault, p.IsRetail, p.SortOrder, p.IsActive);
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<ActionResult<PriceTypeDto>> Create([FromBody] PriceTypeInput input, CancellationToken ct)
|
||||
{
|
||||
if (input.IsDefault)
|
||||
{
|
||||
await _db.PriceTypes.Where(p => p.IsDefault).ExecuteUpdateAsync(s => s.SetProperty(p => p.IsDefault, false), ct);
|
||||
}
|
||||
var e = new PriceType
|
||||
{
|
||||
Name = input.Name, IsDefault = input.IsDefault, IsRetail = input.IsRetail,
|
||||
SortOrder = input.SortOrder, IsActive = input.IsActive,
|
||||
};
|
||||
_db.PriceTypes.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id },
|
||||
new PriceTypeDto(e.Id, e.Name, e.IsDefault, e.IsRetail, e.SortOrder, e.IsActive));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] PriceTypeInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.PriceTypes.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
if (input.IsDefault && !e.IsDefault)
|
||||
{
|
||||
await _db.PriceTypes.Where(p => p.IsDefault && p.Id != id).ExecuteUpdateAsync(s => s.SetProperty(p => p.IsDefault, false), ct);
|
||||
}
|
||||
e.Name = input.Name;
|
||||
e.IsDefault = input.IsDefault;
|
||||
e.IsRetail = input.IsRetail;
|
||||
e.SortOrder = input.SortOrder;
|
||||
e.IsActive = input.IsActive;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.PriceTypes.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
_db.PriceTypes.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/product-groups")]
|
||||
public class ProductGroupsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public ProductGroupsController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<ProductGroupDto>>> List(
|
||||
[FromQuery] PagedRequest req,
|
||||
[FromQuery] Guid? parentId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var q = _db.ProductGroups.AsNoTracking().AsQueryable();
|
||||
if (parentId is not null)
|
||||
{
|
||||
q = parentId == Guid.Empty ? q.Where(g => g.ParentId == null) : q.Where(g => g.ParentId == parentId);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(g => g.Name.ToLower().Contains(s) || g.Path.ToLower().Contains(s));
|
||||
}
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderBy(g => g.Path).ThenBy(g => g.SortOrder).ThenBy(g => g.Name)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(g => new ProductGroupDto(g.Id, g.Name, g.ParentId, g.Path, g.SortOrder, g.IsActive))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<ProductGroupDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<ProductGroupDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var g = await _db.ProductGroups.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
return g is null ? NotFound() : new ProductGroupDto(g.Id, g.Name, g.ParentId, g.Path, g.SortOrder, g.IsActive);
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<ActionResult<ProductGroupDto>> Create([FromBody] ProductGroupInput input, CancellationToken ct)
|
||||
{
|
||||
var path = await BuildPathAsync(input.ParentId, input.Name, ct);
|
||||
var e = new ProductGroup
|
||||
{
|
||||
Name = input.Name, ParentId = input.ParentId, Path = path,
|
||||
SortOrder = input.SortOrder, IsActive = input.IsActive,
|
||||
};
|
||||
_db.ProductGroups.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id },
|
||||
new ProductGroupDto(e.Id, e.Name, e.ParentId, e.Path, e.SortOrder, e.IsActive));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] ProductGroupInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.ProductGroups.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
if (input.ParentId == id) return BadRequest(new { error = "ParentId cannot be self" });
|
||||
|
||||
e.Name = input.Name;
|
||||
e.ParentId = input.ParentId;
|
||||
e.Path = await BuildPathAsync(input.ParentId, input.Name, ct);
|
||||
e.SortOrder = input.SortOrder;
|
||||
e.IsActive = input.IsActive;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
var hasChildren = await _db.ProductGroups.AnyAsync(g => g.ParentId == id, ct);
|
||||
if (hasChildren) return BadRequest(new { error = "Нельзя удалить группу с подгруппами" });
|
||||
var hasProducts = await _db.Products.AnyAsync(p => p.ProductGroupId == id, ct);
|
||||
if (hasProducts) return BadRequest(new { error = "Нельзя удалить группу с товарами" });
|
||||
|
||||
var e = await _db.ProductGroups.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
_db.ProductGroups.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private async Task<string> BuildPathAsync(Guid? parentId, string name, CancellationToken ct)
|
||||
{
|
||||
if (parentId is null) return name;
|
||||
var parent = await _db.ProductGroups.AsNoTracking().FirstOrDefaultAsync(g => g.Id == parentId, ct);
|
||||
return parent is null ? name : $"{parent.Path}/{name}";
|
||||
}
|
||||
}
|
||||
162
src/food-market.api/Controllers/Catalog/ProductsController.cs
Normal file
162
src/food-market.api/Controllers/Catalog/ProductsController.cs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/products")]
|
||||
public class ProductsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public ProductsController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<ProductDto>>> List(
|
||||
[FromQuery] PagedRequest req,
|
||||
[FromQuery] Guid? groupId,
|
||||
[FromQuery] bool? isService,
|
||||
[FromQuery] bool? isWeighed,
|
||||
[FromQuery] bool? isActive,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var q = QueryIncludes().AsNoTracking();
|
||||
if (groupId is not null) q = q.Where(p => p.ProductGroupId == groupId);
|
||||
if (isService is not null) q = q.Where(p => p.IsService == isService);
|
||||
if (isWeighed is not null) q = q.Where(p => p.IsWeighed == isWeighed);
|
||||
if (isActive is not null) q = q.Where(p => p.IsActive == isActive);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(p =>
|
||||
p.Name.ToLower().Contains(s) ||
|
||||
(p.Article != null && p.Article.ToLower().Contains(s)) ||
|
||||
p.Barcodes.Any(b => b.Code.Contains(s)));
|
||||
}
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderBy(p => p.Name)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(Projection)
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<ProductDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<ProductDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var p = await QueryIncludes().AsNoTracking().Where(x => x.Id == id).Select(Projection).FirstOrDefaultAsync(ct);
|
||||
return p is null ? NotFound() : p;
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")]
|
||||
public async Task<ActionResult<ProductDto>> Create([FromBody] ProductInput input, CancellationToken ct)
|
||||
{
|
||||
var e = new Product();
|
||||
Apply(e, input);
|
||||
|
||||
foreach (var b in input.Barcodes ?? [])
|
||||
e.Barcodes.Add(new ProductBarcode { Code = b.Code, Type = b.Type, IsPrimary = b.IsPrimary });
|
||||
foreach (var pr in input.Prices ?? [])
|
||||
e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = pr.Amount, CurrencyId = pr.CurrencyId });
|
||||
|
||||
_db.Products.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
var dto = await GetInternalAsync(e.Id, ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id }, dto);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager,Storekeeper")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] ProductInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.Products
|
||||
.Include(p => p.Barcodes)
|
||||
.Include(p => p.Prices)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
|
||||
Apply(e, input);
|
||||
|
||||
_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 });
|
||||
|
||||
_db.ProductPrices.RemoveRange(e.Prices);
|
||||
e.Prices.Clear();
|
||||
foreach (var pr in input.Prices ?? [])
|
||||
e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = pr.Amount, CurrencyId = pr.CurrencyId });
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.Products.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
_db.Products.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private IQueryable<Product> QueryIncludes() => _db.Products
|
||||
.Include(p => p.UnitOfMeasure)
|
||||
.Include(p => p.VatRate)
|
||||
.Include(p => p.ProductGroup)
|
||||
.Include(p => p.DefaultSupplier)
|
||||
.Include(p => p.CountryOfOrigin)
|
||||
.Include(p => p.PurchaseCurrency)
|
||||
.Include(p => p.Barcodes)
|
||||
.Include(p => p.Prices).ThenInclude(pr => pr.PriceType)
|
||||
.Include(p => p.Prices).ThenInclude(pr => pr.Currency);
|
||||
|
||||
private async Task<ProductDto> GetInternalAsync(Guid id, CancellationToken ct) =>
|
||||
await QueryIncludes().AsNoTracking().Where(x => x.Id == id).Select(Projection).FirstAsync(ct);
|
||||
|
||||
private static readonly System.Linq.Expressions.Expression<Func<Product, ProductDto>> Projection = p =>
|
||||
new ProductDto(
|
||||
p.Id, p.Name, p.Article, p.Description,
|
||||
p.UnitOfMeasureId, p.UnitOfMeasure!.Symbol,
|
||||
p.VatRateId, p.VatRate!.Percent,
|
||||
p.ProductGroupId, p.ProductGroup != null ? p.ProductGroup.Name : null,
|
||||
p.DefaultSupplierId, p.DefaultSupplier != null ? p.DefaultSupplier.Name : null,
|
||||
p.CountryOfOriginId, p.CountryOfOrigin != null ? p.CountryOfOrigin.Name : null,
|
||||
p.IsService, p.IsWeighed, p.IsAlcohol, p.IsMarked,
|
||||
p.MinStock, p.MaxStock,
|
||||
p.PurchasePrice, p.PurchaseCurrencyId, p.PurchaseCurrency != null ? p.PurchaseCurrency.Code : null,
|
||||
p.ImageUrl, p.IsActive,
|
||||
p.Prices.Select(pr => new ProductPriceDto(pr.Id, pr.PriceTypeId, pr.PriceType!.Name, pr.Amount, pr.CurrencyId, pr.Currency!.Code)).ToList(),
|
||||
p.Barcodes.Select(b => new ProductBarcodeDto(b.Id, b.Code, b.Type, b.IsPrimary)).ToList());
|
||||
|
||||
private static void Apply(Product e, ProductInput i)
|
||||
{
|
||||
e.Name = i.Name;
|
||||
e.Article = i.Article;
|
||||
e.Description = i.Description;
|
||||
e.UnitOfMeasureId = i.UnitOfMeasureId;
|
||||
e.VatRateId = i.VatRateId;
|
||||
e.ProductGroupId = i.ProductGroupId;
|
||||
e.DefaultSupplierId = i.DefaultSupplierId;
|
||||
e.CountryOfOriginId = i.CountryOfOriginId;
|
||||
e.IsService = i.IsService;
|
||||
e.IsWeighed = i.IsWeighed;
|
||||
e.IsAlcohol = i.IsAlcohol;
|
||||
e.IsMarked = i.IsMarked;
|
||||
e.MinStock = i.MinStock;
|
||||
e.MaxStock = i.MaxStock;
|
||||
e.PurchasePrice = i.PurchasePrice;
|
||||
e.PurchaseCurrencyId = i.PurchaseCurrencyId;
|
||||
e.ImageUrl = i.ImageUrl;
|
||||
e.IsActive = i.IsActive;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/retail-points")]
|
||||
public class RetailPointsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public RetailPointsController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<RetailPointDto>>> List([FromQuery] PagedRequest req, CancellationToken ct)
|
||||
{
|
||||
var q = _db.RetailPoints.Include(r => r.Store).AsNoTracking().AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(r => r.Name.ToLower().Contains(s) || (r.Code != null && r.Code.ToLower().Contains(s)));
|
||||
}
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderBy(r => r.Name)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(r => new RetailPointDto(r.Id, r.Name, r.Code, r.StoreId, r.Store!.Name,
|
||||
r.Address, r.Phone, r.FiscalSerial, r.FiscalRegNumber, r.IsActive))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<RetailPointDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<RetailPointDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var r = await _db.RetailPoints.Include(x => x.Store).AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
return r is null ? NotFound() : new RetailPointDto(r.Id, r.Name, r.Code, r.StoreId, r.Store!.Name,
|
||||
r.Address, r.Phone, r.FiscalSerial, r.FiscalRegNumber, r.IsActive);
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<ActionResult<RetailPointDto>> Create([FromBody] RetailPointInput input, CancellationToken ct)
|
||||
{
|
||||
var store = await _db.Stores.FirstOrDefaultAsync(s => s.Id == input.StoreId, ct);
|
||||
if (store is null) return BadRequest(new { error = "Invalid StoreId" });
|
||||
|
||||
var e = new RetailPoint
|
||||
{
|
||||
Name = input.Name, Code = input.Code, StoreId = input.StoreId,
|
||||
Address = input.Address, Phone = input.Phone,
|
||||
FiscalSerial = input.FiscalSerial, FiscalRegNumber = input.FiscalRegNumber,
|
||||
IsActive = input.IsActive,
|
||||
};
|
||||
_db.RetailPoints.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id },
|
||||
new RetailPointDto(e.Id, e.Name, e.Code, e.StoreId, store.Name,
|
||||
e.Address, e.Phone, e.FiscalSerial, e.FiscalRegNumber, e.IsActive));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] RetailPointInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.RetailPoints.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
e.Name = input.Name;
|
||||
e.Code = input.Code;
|
||||
e.StoreId = input.StoreId;
|
||||
e.Address = input.Address;
|
||||
e.Phone = input.Phone;
|
||||
e.FiscalSerial = input.FiscalSerial;
|
||||
e.FiscalRegNumber = input.FiscalRegNumber;
|
||||
e.IsActive = input.IsActive;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.RetailPoints.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
_db.RetailPoints.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
93
src/food-market.api/Controllers/Catalog/StoresController.cs
Normal file
93
src/food-market.api/Controllers/Catalog/StoresController.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/stores")]
|
||||
public class StoresController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public StoresController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<StoreDto>>> List([FromQuery] PagedRequest req, CancellationToken ct)
|
||||
{
|
||||
var q = _db.Stores.AsNoTracking().AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(x => x.Name.ToLower().Contains(s) || (x.Code != null && x.Code.ToLower().Contains(s)));
|
||||
}
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderByDescending(x => x.IsMain).ThenBy(x => x.Name)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(x => new StoreDto(x.Id, x.Name, x.Code, x.Kind, x.Address, x.Phone, x.ManagerName, x.IsMain, x.IsActive))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<StoreDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<StoreDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var x = await _db.Stores.AsNoTracking().FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||
return x is null ? NotFound() : new StoreDto(x.Id, x.Name, x.Code, x.Kind, x.Address, x.Phone, x.ManagerName, x.IsMain, x.IsActive);
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<ActionResult<StoreDto>> Create([FromBody] StoreInput input, CancellationToken ct)
|
||||
{
|
||||
if (input.IsMain)
|
||||
{
|
||||
await _db.Stores.Where(s => s.IsMain).ExecuteUpdateAsync(s => s.SetProperty(x => x.IsMain, false), ct);
|
||||
}
|
||||
var e = new Store
|
||||
{
|
||||
Name = input.Name, Code = input.Code, Kind = input.Kind, Address = input.Address,
|
||||
Phone = input.Phone, ManagerName = input.ManagerName, IsMain = input.IsMain, IsActive = input.IsActive,
|
||||
};
|
||||
_db.Stores.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id },
|
||||
new StoreDto(e.Id, e.Name, e.Code, e.Kind, e.Address, e.Phone, e.ManagerName, e.IsMain, e.IsActive));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] StoreInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.Stores.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
if (input.IsMain && !e.IsMain)
|
||||
{
|
||||
await _db.Stores.Where(s => s.IsMain && s.Id != id).ExecuteUpdateAsync(s => s.SetProperty(x => x.IsMain, false), ct);
|
||||
}
|
||||
e.Name = input.Name;
|
||||
e.Code = input.Code;
|
||||
e.Kind = input.Kind;
|
||||
e.Address = input.Address;
|
||||
e.Phone = input.Phone;
|
||||
e.ManagerName = input.ManagerName;
|
||||
e.IsMain = input.IsMain;
|
||||
e.IsActive = input.IsActive;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.Stores.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
_db.Stores.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/units-of-measure")]
|
||||
public class UnitsOfMeasureController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public UnitsOfMeasureController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<UnitOfMeasureDto>>> List([FromQuery] PagedRequest req, CancellationToken ct)
|
||||
{
|
||||
var q = _db.UnitsOfMeasure.AsNoTracking().AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(u => u.Name.ToLower().Contains(s) || u.Symbol.ToLower().Contains(s) || u.Code.ToLower().Contains(s));
|
||||
}
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderByDescending(u => u.IsBase).ThenBy(u => u.Name)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(u => new UnitOfMeasureDto(u.Id, u.Code, u.Symbol, u.Name, u.DecimalPlaces, u.IsBase, u.IsActive))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<UnitOfMeasureDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<UnitOfMeasureDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var u = await _db.UnitsOfMeasure.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
return u is null ? NotFound() : new UnitOfMeasureDto(u.Id, u.Code, u.Symbol, u.Name, u.DecimalPlaces, u.IsBase, u.IsActive);
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<ActionResult<UnitOfMeasureDto>> Create([FromBody] UnitOfMeasureInput input, CancellationToken ct)
|
||||
{
|
||||
if (input.IsBase)
|
||||
{
|
||||
await _db.UnitsOfMeasure.Where(u => u.IsBase).ExecuteUpdateAsync(s => s.SetProperty(u => u.IsBase, false), ct);
|
||||
}
|
||||
|
||||
var e = new UnitOfMeasure
|
||||
{
|
||||
Code = input.Code,
|
||||
Symbol = input.Symbol,
|
||||
Name = input.Name,
|
||||
DecimalPlaces = input.DecimalPlaces,
|
||||
IsBase = input.IsBase,
|
||||
IsActive = input.IsActive,
|
||||
};
|
||||
_db.UnitsOfMeasure.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id },
|
||||
new UnitOfMeasureDto(e.Id, e.Code, e.Symbol, e.Name, e.DecimalPlaces, e.IsBase, e.IsActive));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UnitOfMeasureInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.UnitsOfMeasure.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
|
||||
if (input.IsBase && !e.IsBase)
|
||||
{
|
||||
await _db.UnitsOfMeasure.Where(u => u.IsBase && u.Id != id).ExecuteUpdateAsync(s => s.SetProperty(u => u.IsBase, false), ct);
|
||||
}
|
||||
|
||||
e.Code = input.Code;
|
||||
e.Symbol = input.Symbol;
|
||||
e.Name = input.Name;
|
||||
e.DecimalPlaces = input.DecimalPlaces;
|
||||
e.IsBase = input.IsBase;
|
||||
e.IsActive = input.IsActive;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.UnitsOfMeasure.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
_db.UnitsOfMeasure.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
101
src/food-market.api/Controllers/Catalog/VatRatesController.cs
Normal file
101
src/food-market.api/Controllers/Catalog/VatRatesController.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using foodmarket.Application.Catalog;
|
||||
using foodmarket.Application.Common;
|
||||
using foodmarket.Domain.Catalog;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foodmarket.Api.Controllers.Catalog;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/catalog/vat-rates")]
|
||||
public class VatRatesController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public VatRatesController(AppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<VatRateDto>>> List([FromQuery] PagedRequest req, CancellationToken ct)
|
||||
{
|
||||
var q = _db.VatRates.AsNoTracking().AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(req.Search))
|
||||
{
|
||||
var s = req.Search.Trim().ToLower();
|
||||
q = q.Where(v => v.Name.ToLower().Contains(s));
|
||||
}
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q
|
||||
.OrderByDescending(v => v.IsDefault).ThenBy(v => v.Percent)
|
||||
.Skip(req.Skip).Take(req.Take)
|
||||
.Select(v => new VatRateDto(v.Id, v.Name, v.Percent, v.IsIncludedInPrice, v.IsDefault, v.IsActive))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<VatRateDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<VatRateDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var v = await _db.VatRates.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
return v is null ? NotFound() : new VatRateDto(v.Id, v.Name, v.Percent, v.IsIncludedInPrice, v.IsDefault, v.IsActive);
|
||||
}
|
||||
|
||||
[HttpPost, Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<ActionResult<VatRateDto>> Create([FromBody] VatRateInput input, CancellationToken ct)
|
||||
{
|
||||
if (input.IsDefault)
|
||||
{
|
||||
await ResetDefaultsAsync(ct);
|
||||
}
|
||||
|
||||
var e = new VatRate
|
||||
{
|
||||
Name = input.Name,
|
||||
Percent = input.Percent,
|
||||
IsIncludedInPrice = input.IsIncludedInPrice,
|
||||
IsDefault = input.IsDefault,
|
||||
IsActive = input.IsActive,
|
||||
};
|
||||
_db.VatRates.Add(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = e.Id },
|
||||
new VatRateDto(e.Id, e.Name, e.Percent, e.IsIncludedInPrice, e.IsDefault, e.IsActive));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] VatRateInput input, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.VatRates.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
|
||||
if (input.IsDefault && !e.IsDefault)
|
||||
{
|
||||
await ResetDefaultsAsync(ct);
|
||||
}
|
||||
|
||||
e.Name = input.Name;
|
||||
e.Percent = input.Percent;
|
||||
e.IsIncludedInPrice = input.IsIncludedInPrice;
|
||||
e.IsDefault = input.IsDefault;
|
||||
e.IsActive = input.IsActive;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
var e = await _db.VatRates.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (e is null) return NotFound();
|
||||
_db.VatRates.Remove(e);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private async Task ResetDefaultsAsync(CancellationToken ct)
|
||||
{
|
||||
await _db.VatRates.Where(v => v.IsDefault).ExecuteUpdateAsync(s => s.SetProperty(v => v.IsDefault, false), ct);
|
||||
}
|
||||
}
|
||||
84
src/food-market.application/Catalog/CatalogDtos.cs
Normal file
84
src/food-market.application/Catalog/CatalogDtos.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
using foodmarket.Domain.Catalog;
|
||||
|
||||
namespace foodmarket.Application.Catalog;
|
||||
|
||||
public record CountryDto(Guid Id, string Code, string Name, int SortOrder);
|
||||
|
||||
public record CurrencyDto(Guid Id, string Code, string Name, string Symbol, int MinorUnit, bool IsActive);
|
||||
|
||||
public record VatRateDto(
|
||||
Guid Id, string Name, decimal Percent, bool IsIncludedInPrice, bool IsDefault, bool IsActive);
|
||||
|
||||
public record UnitOfMeasureDto(
|
||||
Guid Id, string Code, string Symbol, string Name, int DecimalPlaces, bool IsBase, bool IsActive);
|
||||
|
||||
public record PriceTypeDto(
|
||||
Guid Id, string Name, bool IsDefault, bool IsRetail, int SortOrder, bool IsActive);
|
||||
|
||||
public record StoreDto(
|
||||
Guid Id, string Name, string? Code, StoreKind Kind, string? Address, string? Phone,
|
||||
string? ManagerName, bool IsMain, bool IsActive);
|
||||
|
||||
public record RetailPointDto(
|
||||
Guid Id, string Name, string? Code, Guid StoreId, string? StoreName,
|
||||
string? Address, string? Phone, string? FiscalSerial, string? FiscalRegNumber, bool IsActive);
|
||||
|
||||
public record ProductGroupDto(
|
||||
Guid Id, string Name, Guid? ParentId, string Path, int SortOrder, bool IsActive);
|
||||
|
||||
public record CounterpartyDto(
|
||||
Guid Id, string Name, string? LegalName, CounterpartyKind Kind, CounterpartyType Type,
|
||||
string? Bin, string? Iin, string? TaxNumber, Guid? CountryId, string? CountryName,
|
||||
string? Address, string? Phone, string? Email,
|
||||
string? BankName, string? BankAccount, string? Bik, string? ContactPerson, string? Notes, bool IsActive);
|
||||
|
||||
public record ProductBarcodeDto(Guid Id, string Code, BarcodeType Type, bool IsPrimary);
|
||||
|
||||
public record ProductPriceDto(Guid Id, Guid PriceTypeId, string PriceTypeName, decimal Amount, Guid CurrencyId, string CurrencyCode);
|
||||
|
||||
public record ProductDto(
|
||||
Guid Id, string Name, string? Article, string? Description,
|
||||
Guid UnitOfMeasureId, string UnitSymbol,
|
||||
Guid VatRateId, decimal VatPercent,
|
||||
Guid? ProductGroupId, string? ProductGroupName,
|
||||
Guid? DefaultSupplierId, string? DefaultSupplierName,
|
||||
Guid? CountryOfOriginId, string? CountryOfOriginName,
|
||||
bool IsService, bool IsWeighed, bool IsAlcohol, bool IsMarked,
|
||||
decimal? MinStock, decimal? MaxStock,
|
||||
decimal? PurchasePrice, Guid? PurchaseCurrencyId, string? PurchaseCurrencyCode,
|
||||
string? ImageUrl, bool IsActive,
|
||||
IReadOnlyList<ProductPriceDto> Prices,
|
||||
IReadOnlyList<ProductBarcodeDto> Barcodes);
|
||||
|
||||
// Upsert payloads (input)
|
||||
public record CountryInput(string Code, string Name, int SortOrder = 0);
|
||||
public record CurrencyInput(string Code, string Name, string Symbol, int MinorUnit = 2, bool IsActive = true);
|
||||
public record VatRateInput(string Name, decimal Percent, bool IsIncludedInPrice, bool IsDefault = false, bool IsActive = true);
|
||||
public record UnitOfMeasureInput(string Code, string Symbol, string Name, int DecimalPlaces, bool IsBase = false, bool IsActive = true);
|
||||
public record PriceTypeInput(string Name, bool IsDefault = false, bool IsRetail = false, int SortOrder = 0, bool IsActive = true);
|
||||
public record StoreInput(
|
||||
string Name, string? Code, StoreKind Kind = StoreKind.Warehouse,
|
||||
string? Address = null, string? Phone = null, string? ManagerName = null,
|
||||
bool IsMain = false, bool IsActive = true);
|
||||
public record RetailPointInput(
|
||||
string Name, string? Code, Guid StoreId,
|
||||
string? Address = null, string? Phone = null,
|
||||
string? FiscalSerial = null, string? FiscalRegNumber = null, bool IsActive = true);
|
||||
public record ProductGroupInput(string Name, Guid? ParentId, int SortOrder = 0, bool IsActive = true);
|
||||
public record CounterpartyInput(
|
||||
string Name, string? LegalName, CounterpartyKind Kind, CounterpartyType Type,
|
||||
string? Bin, string? Iin, string? TaxNumber, Guid? CountryId,
|
||||
string? Address, string? Phone, string? Email,
|
||||
string? BankName, string? BankAccount, string? Bik, string? ContactPerson, string? Notes, bool IsActive = true);
|
||||
public record ProductBarcodeInput(string Code, BarcodeType Type = BarcodeType.Ean13, bool IsPrimary = false);
|
||||
public record ProductPriceInput(Guid PriceTypeId, decimal Amount, Guid CurrencyId);
|
||||
public record ProductInput(
|
||||
string Name, string? Article, string? Description,
|
||||
Guid UnitOfMeasureId, Guid VatRateId,
|
||||
Guid? ProductGroupId, Guid? DefaultSupplierId, Guid? CountryOfOriginId,
|
||||
bool IsService = false, bool IsWeighed = false, bool IsAlcohol = false, bool IsMarked = false,
|
||||
decimal? MinStock = null, decimal? MaxStock = null,
|
||||
decimal? PurchasePrice = null, Guid? PurchaseCurrencyId = null,
|
||||
string? ImageUrl = null, bool IsActive = true,
|
||||
IReadOnlyList<ProductPriceInput>? Prices = null,
|
||||
IReadOnlyList<ProductBarcodeInput>? Barcodes = null);
|
||||
23
src/food-market.application/Common/PagedRequest.cs
Normal file
23
src/food-market.application/Common/PagedRequest.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
namespace foodmarket.Application.Common;
|
||||
|
||||
public sealed class PagedRequest
|
||||
{
|
||||
public int Page { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 50;
|
||||
public string? Search { get; init; }
|
||||
public string? SortBy { get; init; }
|
||||
public bool SortDesc { get; init; }
|
||||
|
||||
public int Skip => Math.Max(0, (Page - 1) * PageSize);
|
||||
public int Take => Math.Clamp(PageSize, 1, 500);
|
||||
}
|
||||
|
||||
public sealed class PagedResult<T>
|
||||
{
|
||||
public required IReadOnlyList<T> Items { get; init; }
|
||||
public required int Total { get; init; }
|
||||
public required int Page { get; init; }
|
||||
public required int PageSize { get; init; }
|
||||
|
||||
public int TotalPages => PageSize == 0 ? 0 : (int)Math.Ceiling(Total / (double)PageSize);
|
||||
}
|
||||
Loading…
Reference in a new issue