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:
nurdotnet 2026-04-21 19:22:56 +05:00
parent cb66684134
commit 6b86106937
12 changed files with 1123 additions and 0 deletions

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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}";
}
}

View 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;
}
}

View 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/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();
}
}

View 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();
}
}

View file

@ -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();
}
}

View 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);
}
}

View 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);

View 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);
}