feat: Implement Category, Item, UOM, Vendor, and Warehouse services with CRUD operations
- Added CategoryService for managing categories with listing, tree structure, and creation functionalities. - Introduced ItemService for item management, including listing, detail retrieval, creation, updating, and status management. - Created UomService for handling unit of measure operations, including listing and creation. - Developed VendorService for vendor management, supporting listing, detail retrieval, creation, updating, and status management. - Implemented WarehouseService for warehouse and bin management, including listing warehouses, creating warehouses, and managing bins within warehouses. - Added interfaces for each service to define the contract for service implementations. - Generated Entity Framework Core model snapshot for database migrations.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Dtos.Categories;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
public sealed class CategoryService : ICategoryService
|
||||
{
|
||||
private readonly IRepository<Category> _categories;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public CategoryService(IRepository<Category> categories, IUnitOfWork uow)
|
||||
{
|
||||
_categories = categories;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<CategoryDto>> ListAsync(PageQuery query, CancellationToken ct = default)
|
||||
{
|
||||
var q = _categories.Query().AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(c => EF.Functions.ILike(c.Name, $"%{term}%"));
|
||||
}
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderBy(c => c.Name)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(c => new CategoryDto(c.CategoryId, c.Name, c.ParentId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<CategoryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CategoryTreeDto>> GetTreeAsync(CancellationToken ct = default)
|
||||
{
|
||||
var all = await _categories.Query().AsNoTracking()
|
||||
.OrderBy(c => c.Name)
|
||||
.Select(c => new CategoryDto(c.CategoryId, c.Name, c.ParentId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
var byParent = all.ToLookup(c => c.ParentId);
|
||||
|
||||
List<CategoryTreeDto> Build(long? parentId) =>
|
||||
byParent[parentId]
|
||||
.Select(c => new CategoryTreeDto(c.CategoryId, c.Name, c.ParentId, Build(c.CategoryId)))
|
||||
.ToList();
|
||||
|
||||
return Build(null);
|
||||
}
|
||||
|
||||
public async Task<CategoryDto> CreateAsync(CreateCategoryRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (request.ParentId is not null
|
||||
&& !await _categories.Query().AnyAsync(c => c.CategoryId == request.ParentId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"Parent category {request.ParentId} does not exist.", 422);
|
||||
|
||||
var category = new Category { Name = request.Name.Trim(), ParentId = request.ParentId };
|
||||
await _categories.AddAsync(category, ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
return new CategoryDto(category.CategoryId, category.Name, category.ParentId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using ERPCore.Dtos.Categories;
|
||||
using ERPCore.Dtos.Common;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>Category master business logic (docs/11-BACKEND-PHASE1.md §2.3).</summary>
|
||||
public interface ICategoryService
|
||||
{
|
||||
Task<PagedResponse<CategoryDto>> ListAsync(PageQuery query, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<CategoryTreeDto>> GetTreeAsync(CancellationToken ct = default);
|
||||
Task<CategoryDto> CreateAsync(CreateCategoryRequest request, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Items;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Item master business logic (docs/11-BACKEND-PHASE1.md §2.1–2.2). Returns DTOs;
|
||||
/// entities never cross this boundary (00-CORE §4).
|
||||
/// </summary>
|
||||
public interface IItemService
|
||||
{
|
||||
Task<PagedResponse<ItemListItemDto>> ListAsync(
|
||||
PageQuery query, EntityStatus? status, long? categoryId, TrackingMode? trackingMode, CancellationToken ct = default);
|
||||
|
||||
Task<ETagged<ItemDetailDto>?> GetAsync(long itemId, CancellationToken ct = default);
|
||||
|
||||
Task<ETagged<ItemDetailDto>> CreateAsync(CreateItemRequest request, CancellationToken ct = default);
|
||||
|
||||
Task<ETagged<ItemDetailDto>> UpdateAsync(long itemId, UpdateItemRequest request, uint expectedRowVersion, CancellationToken ct = default);
|
||||
|
||||
Task SetStatusAsync(long itemId, EntityStatus status, CancellationToken ct = default);
|
||||
|
||||
Task<ItemReorderSettingsDto> UpdateReorderAsync(long itemId, UpdateReorderRequest request, CancellationToken ct = default);
|
||||
|
||||
Task<ItemUomConversionsDto> UpdateUomConversionsAsync(long itemId, UpdateUomConversionsRequest request, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Uoms;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>UOM master business logic (docs/11-BACKEND-PHASE1.md §2.2).</summary>
|
||||
public interface IUomService
|
||||
{
|
||||
Task<PagedResponse<UomDto>> ListAsync(PageQuery query, CancellationToken ct = default);
|
||||
Task<UomDto> CreateAsync(CreateUomRequest request, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Vendors;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>Vendor master business logic (docs/11-BACKEND-PHASE1.md §2.4).</summary>
|
||||
public interface IVendorService
|
||||
{
|
||||
Task<PagedResponse<VendorDto>> ListAsync(PageQuery query, EntityStatus? status, CancellationToken ct = default);
|
||||
Task<ETagged<VendorDto>?> GetAsync(long vendorId, CancellationToken ct = default);
|
||||
Task<ETagged<VendorDto>> CreateAsync(CreateVendorRequest request, CancellationToken ct = default);
|
||||
Task<ETagged<VendorDto>> UpdateAsync(long vendorId, UpdateVendorRequest request, uint expectedRowVersion, CancellationToken ct = default);
|
||||
Task SetStatusAsync(long vendorId, EntityStatus status, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Warehouses;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>Warehouse & bin master business logic (docs/11-BACKEND-PHASE1.md §2.5).</summary>
|
||||
public interface IWarehouseService
|
||||
{
|
||||
Task<PagedResponse<WarehouseDto>> ListAsync(PageQuery query, CancellationToken ct = default);
|
||||
Task<WarehouseDto?> GetAsync(long warehouseId, CancellationToken ct = default);
|
||||
Task<WarehouseDto> CreateAsync(CreateWarehouseRequest request, CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<BinDto>> ListBinsAsync(long warehouseId, CancellationToken ct = default);
|
||||
Task<BinDto> CreateBinAsync(long warehouseId, CreateBinRequest request, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Items;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Item master service. Enforces SKU uniqueness (SKU_DUPLICATE), reference
|
||||
/// integrity, and optimistic concurrency (CONCURRENCY_CONFLICT) per
|
||||
/// docs/11-BACKEND-PHASE1.md §2.1–2.2 and 02-SECURITY C.1.
|
||||
/// </summary>
|
||||
public sealed class ItemService : IItemService
|
||||
{
|
||||
private readonly IRepository<Item> _items;
|
||||
private readonly IRepository<Category> _categories;
|
||||
private readonly IRepository<Uom> _uoms;
|
||||
private readonly IRepository<Vendor> _vendors;
|
||||
private readonly IRepository<Warehouse> _warehouses;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public ItemService(
|
||||
IRepository<Item> items,
|
||||
IRepository<Category> categories,
|
||||
IRepository<Uom> uoms,
|
||||
IRepository<Vendor> vendors,
|
||||
IRepository<Warehouse> warehouses,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
_items = items;
|
||||
_categories = categories;
|
||||
_uoms = uoms;
|
||||
_vendors = vendors;
|
||||
_warehouses = warehouses;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<ItemListItemDto>> ListAsync(
|
||||
PageQuery query, EntityStatus? status, long? categoryId, TrackingMode? trackingMode, CancellationToken ct = default)
|
||||
{
|
||||
var q = _items.Query().AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(i => EF.Functions.ILike(i.Sku, $"%{term}%") || EF.Functions.ILike(i.Name, $"%{term}%"));
|
||||
}
|
||||
if (status is not null) q = q.Where(i => i.Status == status);
|
||||
if (categoryId is not null) q = q.Where(i => i.CategoryId == categoryId);
|
||||
if (trackingMode is not null) q = q.Where(i => i.TrackingMode == trackingMode);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderBy(i => i.Sku)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(i => new ItemListItemDto(
|
||||
i.ItemId, i.Sku, i.Name, i.CategoryId, i.BaseUomId, i.DefaultVendorId,
|
||||
i.ItemType, i.TrackingMode, i.TaxClass, i.Status))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<ItemListItemDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<ETagged<ItemDetailDto>?> GetAsync(long itemId, CancellationToken ct = default)
|
||||
{
|
||||
var item = await _items.Query().AsNoTracking()
|
||||
.Include(i => i.ReorderSettings)
|
||||
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct);
|
||||
|
||||
return item is null ? null : new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
|
||||
}
|
||||
|
||||
public async Task<ETagged<ItemDetailDto>> CreateAsync(CreateItemRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (await _items.Query().AnyAsync(i => i.Sku == request.Sku, ct))
|
||||
throw new DomainException(ErrorCodes.SkuDuplicate, $"An item with SKU '{request.Sku}' already exists.", 400);
|
||||
|
||||
await ValidateReferencesAsync(request.CategoryId, request.BaseUomId, request.DefaultVendorId, ct);
|
||||
|
||||
var item = new Item
|
||||
{
|
||||
Sku = request.Sku.Trim(),
|
||||
Name = request.Name.Trim(),
|
||||
Description = request.Description,
|
||||
CategoryId = request.CategoryId,
|
||||
BaseUomId = request.BaseUomId,
|
||||
DefaultVendorId = request.DefaultVendorId,
|
||||
ItemType = request.ItemType,
|
||||
TrackingMode = request.TrackingMode,
|
||||
TaxClass = request.TaxClass,
|
||||
Status = EntityStatus.Active,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _items.AddAsync(item, ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
return new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
|
||||
}
|
||||
|
||||
public async Task<ETagged<ItemDetailDto>> UpdateAsync(
|
||||
long itemId, UpdateItemRequest request, uint expectedRowVersion, CancellationToken ct = default)
|
||||
{
|
||||
var item = await _items.Query()
|
||||
.Include(i => i.ReorderSettings)
|
||||
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
|
||||
?? throw new NotFoundException($"Item {itemId} was not found.");
|
||||
|
||||
if (item.RowVersion != expectedRowVersion)
|
||||
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The item was modified by another request.", 412);
|
||||
|
||||
if (!string.Equals(item.Sku, request.Sku, StringComparison.Ordinal)
|
||||
&& await _items.Query().AnyAsync(i => i.Sku == request.Sku && i.ItemId != itemId, ct))
|
||||
throw new DomainException(ErrorCodes.SkuDuplicate, $"An item with SKU '{request.Sku}' already exists.", 400);
|
||||
|
||||
await ValidateReferencesAsync(request.CategoryId, request.BaseUomId, request.DefaultVendorId, ct);
|
||||
|
||||
item.Sku = request.Sku.Trim();
|
||||
item.Name = request.Name.Trim();
|
||||
item.Description = request.Description;
|
||||
item.CategoryId = request.CategoryId;
|
||||
item.BaseUomId = request.BaseUomId;
|
||||
item.DefaultVendorId = request.DefaultVendorId;
|
||||
item.ItemType = request.ItemType;
|
||||
item.TrackingMode = request.TrackingMode;
|
||||
item.TaxClass = request.TaxClass;
|
||||
item.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await SaveGuardingConcurrencyAsync(ct);
|
||||
return new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
|
||||
}
|
||||
|
||||
public async Task SetStatusAsync(long itemId, EntityStatus status, CancellationToken ct = default)
|
||||
{
|
||||
var item = await _items.GetByIdAsync(itemId, ct)
|
||||
?? throw new NotFoundException($"Item {itemId} was not found.");
|
||||
|
||||
item.Status = status;
|
||||
item.UpdatedAt = DateTime.UtcNow;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<ItemReorderSettingsDto> UpdateReorderAsync(
|
||||
long itemId, UpdateReorderRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (request.Settings.Select(s => s.WarehouseId).Distinct().Count() != request.Settings.Count)
|
||||
throw new DomainException(ErrorCodes.Validation, "Duplicate warehouseId in reorder settings.", 400);
|
||||
|
||||
var item = await _items.Query()
|
||||
.Include(i => i.ReorderSettings)
|
||||
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
|
||||
?? throw new NotFoundException($"Item {itemId} was not found.");
|
||||
|
||||
foreach (var warehouseId in request.Settings.Select(s => s.WarehouseId))
|
||||
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == warehouseId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"Warehouse {warehouseId} does not exist.", 422);
|
||||
|
||||
// Full-replacement upsert (avoids delete+insert clashes on the unique index).
|
||||
foreach (var stale in item.ReorderSettings.Where(r => request.Settings.All(s => s.WarehouseId != r.WarehouseId)).ToList())
|
||||
item.ReorderSettings.Remove(stale);
|
||||
foreach (var input in request.Settings)
|
||||
{
|
||||
var existing = item.ReorderSettings.FirstOrDefault(r => r.WarehouseId == input.WarehouseId);
|
||||
if (existing is null)
|
||||
{
|
||||
item.ReorderSettings.Add(new ItemReorder
|
||||
{
|
||||
ItemId = itemId,
|
||||
WarehouseId = input.WarehouseId,
|
||||
ReorderPoint = input.ReorderPoint,
|
||||
ReorderQty = input.ReorderQty
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.ReorderPoint = input.ReorderPoint;
|
||||
existing.ReorderQty = input.ReorderQty;
|
||||
}
|
||||
}
|
||||
|
||||
item.UpdatedAt = DateTime.UtcNow;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
var settings = item.ReorderSettings
|
||||
.OrderBy(r => r.WarehouseId)
|
||||
.Select(r => new ItemReorderDto(r.WarehouseId, r.ReorderPoint, r.ReorderQty))
|
||||
.ToList();
|
||||
return new ItemReorderSettingsDto(settings);
|
||||
}
|
||||
|
||||
public async Task<ItemUomConversionsDto> UpdateUomConversionsAsync(
|
||||
long itemId, UpdateUomConversionsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var pairs = request.Conversions.Select(c => (c.FromUom, c.ToUom)).ToList();
|
||||
if (pairs.Distinct().Count() != pairs.Count)
|
||||
throw new DomainException(ErrorCodes.Validation, "Duplicate (fromUom, toUom) in conversions.", 400);
|
||||
|
||||
var item = await _items.Query()
|
||||
.Include(i => i.UomConversions)
|
||||
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
|
||||
?? throw new NotFoundException($"Item {itemId} was not found.");
|
||||
|
||||
foreach (var uomId in request.Conversions.SelectMany(c => new[] { c.FromUom, c.ToUom }).Distinct())
|
||||
if (!await _uoms.Query().AnyAsync(u => u.UomId == uomId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"UOM {uomId} does not exist.", 422);
|
||||
|
||||
foreach (var stale in item.UomConversions.Where(c => request.Conversions.All(r => r.FromUom != c.FromUomId || r.ToUom != c.ToUomId)).ToList())
|
||||
item.UomConversions.Remove(stale);
|
||||
foreach (var input in request.Conversions)
|
||||
{
|
||||
var existing = item.UomConversions.FirstOrDefault(c => c.FromUomId == input.FromUom && c.ToUomId == input.ToUom);
|
||||
if (existing is null)
|
||||
{
|
||||
item.UomConversions.Add(new UomConversion
|
||||
{
|
||||
ItemId = itemId,
|
||||
FromUomId = input.FromUom,
|
||||
ToUomId = input.ToUom,
|
||||
Factor = input.Factor
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Factor = input.Factor;
|
||||
}
|
||||
}
|
||||
|
||||
item.UpdatedAt = DateTime.UtcNow;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
var conversions = item.UomConversions
|
||||
.OrderBy(c => c.ConversionId)
|
||||
.Select(c => new UomConversionDto(c.ConversionId, c.FromUomId, c.ToUomId, c.Factor))
|
||||
.ToList();
|
||||
return new ItemUomConversionsDto(item.ItemId, item.BaseUomId, conversions);
|
||||
}
|
||||
|
||||
private async Task ValidateReferencesAsync(long categoryId, long baseUomId, long? defaultVendorId, CancellationToken ct)
|
||||
{
|
||||
if (!await _categories.Query().AnyAsync(c => c.CategoryId == categoryId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"Category {categoryId} does not exist.", 422);
|
||||
|
||||
if (!await _uoms.Query().AnyAsync(u => u.UomId == baseUomId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"UOM {baseUomId} does not exist.", 422);
|
||||
|
||||
if (defaultVendorId is not null)
|
||||
{
|
||||
var vendor = await _vendors.Query().AsNoTracking()
|
||||
.FirstOrDefaultAsync(v => v.VendorId == defaultVendorId, ct);
|
||||
if (vendor is null)
|
||||
throw new DomainException(ErrorCodes.Validation, $"Vendor {defaultVendorId} does not exist.", 422);
|
||||
if (vendor.Status != EntityStatus.Active)
|
||||
throw new DomainException(ErrorCodes.Validation, $"Vendor {defaultVendorId} is inactive.", 422);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveGuardingConcurrencyAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The item was modified by another request.", 412);
|
||||
}
|
||||
}
|
||||
|
||||
private static ItemDetailDto ToDetail(Item i) => new(
|
||||
i.ItemId, i.Sku, i.Name, i.Description, i.CategoryId, i.BaseUomId, i.DefaultVendorId,
|
||||
i.ItemType, i.TrackingMode, i.TaxClass, i.Status,
|
||||
i.ReorderSettings
|
||||
.OrderBy(r => r.WarehouseId)
|
||||
.Select(r => new ItemReorderDto(r.WarehouseId, r.ReorderPoint, r.ReorderQty))
|
||||
.ToList(),
|
||||
i.CreatedAt, i.UpdatedAt);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Uoms;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
public sealed class UomService : IUomService
|
||||
{
|
||||
private readonly IRepository<Uom> _uoms;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public UomService(IRepository<Uom> uoms, IUnitOfWork uow)
|
||||
{
|
||||
_uoms = uoms;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<UomDto>> ListAsync(PageQuery query, CancellationToken ct = default)
|
||||
{
|
||||
var q = _uoms.Query().AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(u => EF.Functions.ILike(u.Name, $"%{term}%"));
|
||||
}
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderBy(u => u.Name)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(u => new UomDto(u.UomId, u.Name))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<UomDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<UomDto> CreateAsync(CreateUomRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var name = request.Name.Trim();
|
||||
if (await _uoms.Query().AnyAsync(u => u.Name == name, ct))
|
||||
throw new ConflictException($"A UOM named '{name}' already exists.");
|
||||
|
||||
var uom = new Uom { Name = name };
|
||||
await _uoms.AddAsync(uom, ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
return new UomDto(uom.UomId, uom.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Vendors;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
public sealed class VendorService : IVendorService
|
||||
{
|
||||
private readonly IRepository<Vendor> _vendors;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public VendorService(IRepository<Vendor> vendors, IUnitOfWork uow)
|
||||
{
|
||||
_vendors = vendors;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<VendorDto>> ListAsync(PageQuery query, EntityStatus? status, CancellationToken ct = default)
|
||||
{
|
||||
var q = _vendors.Query().AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(v => EF.Functions.ILike(v.Code, $"%{term}%") || EF.Functions.ILike(v.Name, $"%{term}%"));
|
||||
}
|
||||
if (status is not null) q = q.Where(v => v.Status == status);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderBy(v => v.Code)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<VendorDto>.Create(rows.Select(Map).ToList(), query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<ETagged<VendorDto>?> GetAsync(long vendorId, CancellationToken ct = default)
|
||||
{
|
||||
var vendor = await _vendors.Query().AsNoTracking()
|
||||
.FirstOrDefaultAsync(v => v.VendorId == vendorId, ct);
|
||||
return vendor is null ? null : new ETagged<VendorDto>(Map(vendor), vendor.RowVersion);
|
||||
}
|
||||
|
||||
public async Task<ETagged<VendorDto>> CreateAsync(CreateVendorRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var code = request.Code.Trim();
|
||||
if (await _vendors.Query().AnyAsync(v => v.Code == code, ct))
|
||||
throw new ConflictException($"A vendor with code '{code}' already exists.");
|
||||
|
||||
var vendor = new Vendor
|
||||
{
|
||||
Code = code,
|
||||
Name = request.Name.Trim(),
|
||||
Terms = request.Terms,
|
||||
TaxReg = request.TaxReg,
|
||||
Currency = request.Currency.Trim().ToUpperInvariant(),
|
||||
Status = EntityStatus.Active,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _vendors.AddAsync(vendor, ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
return new ETagged<VendorDto>(Map(vendor), vendor.RowVersion);
|
||||
}
|
||||
|
||||
public async Task<ETagged<VendorDto>> UpdateAsync(
|
||||
long vendorId, UpdateVendorRequest request, uint expectedRowVersion, CancellationToken ct = default)
|
||||
{
|
||||
var vendor = await _vendors.GetByIdAsync(vendorId, ct)
|
||||
?? throw new NotFoundException($"Vendor {vendorId} was not found.");
|
||||
|
||||
if (vendor.RowVersion != expectedRowVersion)
|
||||
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The vendor was modified by another request.", 412);
|
||||
|
||||
var code = request.Code.Trim();
|
||||
if (!string.Equals(vendor.Code, code, StringComparison.Ordinal)
|
||||
&& await _vendors.Query().AnyAsync(v => v.Code == code && v.VendorId != vendorId, ct))
|
||||
throw new ConflictException($"A vendor with code '{code}' already exists.");
|
||||
|
||||
vendor.Code = code;
|
||||
vendor.Name = request.Name.Trim();
|
||||
vendor.Terms = request.Terms;
|
||||
vendor.TaxReg = request.TaxReg;
|
||||
vendor.Currency = request.Currency.Trim().ToUpperInvariant();
|
||||
vendor.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The vendor was modified by another request.", 412);
|
||||
}
|
||||
|
||||
return new ETagged<VendorDto>(Map(vendor), vendor.RowVersion);
|
||||
}
|
||||
|
||||
public async Task SetStatusAsync(long vendorId, EntityStatus status, CancellationToken ct = default)
|
||||
{
|
||||
var vendor = await _vendors.GetByIdAsync(vendorId, ct)
|
||||
?? throw new NotFoundException($"Vendor {vendorId} was not found.");
|
||||
|
||||
vendor.Status = status;
|
||||
vendor.UpdatedAt = DateTime.UtcNow;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
private static VendorDto Map(Vendor v) => new(
|
||||
v.VendorId, v.Code, v.Name, v.Terms, v.TaxReg, v.Currency, v.Status, v.CreatedAt, v.UpdatedAt);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Warehouses;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
public sealed class WarehouseService : IWarehouseService
|
||||
{
|
||||
private readonly IRepository<Warehouse> _warehouses;
|
||||
private readonly IRepository<Bin> _bins;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public WarehouseService(IRepository<Warehouse> warehouses, IRepository<Bin> bins, IUnitOfWork uow)
|
||||
{
|
||||
_warehouses = warehouses;
|
||||
_bins = bins;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<WarehouseDto>> ListAsync(PageQuery query, CancellationToken ct = default)
|
||||
{
|
||||
var q = _warehouses.Query().AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(w => EF.Functions.ILike(w.Code, $"%{term}%") || EF.Functions.ILike(w.Name, $"%{term}%"));
|
||||
}
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderBy(w => w.Code)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(w => new WarehouseDto(w.WarehouseId, w.Code, w.Name))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<WarehouseDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<WarehouseDto?> GetAsync(long warehouseId, CancellationToken ct = default)
|
||||
{
|
||||
var w = await _warehouses.Query().AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.WarehouseId == warehouseId, ct);
|
||||
return w is null ? null : new WarehouseDto(w.WarehouseId, w.Code, w.Name);
|
||||
}
|
||||
|
||||
public async Task<WarehouseDto> CreateAsync(CreateWarehouseRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var code = request.Code.Trim();
|
||||
if (await _warehouses.Query().AnyAsync(w => w.Code == code, ct))
|
||||
throw new ConflictException($"A warehouse with code '{code}' already exists.");
|
||||
|
||||
var warehouse = new Warehouse { Code = code, Name = request.Name.Trim() };
|
||||
await _warehouses.AddAsync(warehouse, ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
return new WarehouseDto(warehouse.WarehouseId, warehouse.Code, warehouse.Name);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<BinDto>> ListBinsAsync(long warehouseId, CancellationToken ct = default)
|
||||
{
|
||||
await EnsureWarehouseExistsAsync(warehouseId, ct);
|
||||
|
||||
return await _bins.Query().AsNoTracking()
|
||||
.Where(b => b.WarehouseId == warehouseId)
|
||||
.OrderBy(b => b.Code)
|
||||
.Select(b => new BinDto(b.BinId, b.WarehouseId, b.Code, b.BinType))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<BinDto> CreateBinAsync(long warehouseId, CreateBinRequest request, CancellationToken ct = default)
|
||||
{
|
||||
await EnsureWarehouseExistsAsync(warehouseId, ct);
|
||||
|
||||
var code = request.Code.Trim();
|
||||
if (await _bins.Query().AnyAsync(b => b.WarehouseId == warehouseId && b.Code == code, ct))
|
||||
throw new ConflictException($"Bin '{code}' already exists in warehouse {warehouseId}.");
|
||||
|
||||
var bin = new Bin { WarehouseId = warehouseId, Code = code, BinType = request.BinType };
|
||||
await _bins.AddAsync(bin, ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
return new BinDto(bin.BinId, bin.WarehouseId, bin.Code, bin.BinType);
|
||||
}
|
||||
|
||||
private async Task EnsureWarehouseExistsAsync(long warehouseId, CancellationToken ct)
|
||||
{
|
||||
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == warehouseId, ct))
|
||||
throw new NotFoundException($"Warehouse {warehouseId} was not found.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user