item mng with sync

This commit is contained in:
Kalana B. Thilakarathna
2026-08-19 12:49:04 +05:30
parent 45f374133f
commit 397af8fd59
42 changed files with 5191 additions and 16 deletions
@@ -0,0 +1,13 @@
namespace Distribution.Features.Item;
public interface IItemRepository
{
Task<Models.Item?> GetByIdAsync(Guid id);
Task<List<Models.Item>> GetAllAsync(Guid? categoryId = null, Guid? subCategoryId = null, Guid? brandId = null);
Task<bool> SkuExistsAsync(string sku, Guid? excludeId = null);
Task<Models.Item?> GetByExternalErpItemIdAsync(int erpId);
Task AddAsync(Models.Item entity);
void Update(Models.Item entity);
void Remove(Models.Item entity);
Task ReplaceConversionsAsync(Guid itemId, List<Models.UomConversion> conversions);
}
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Item;
[ApiController]
[Route("api/items")]
[Authorize]
public class ItemController(ItemService service) : ControllerBase
{
[HttpGet]
[Authorize(Policy = "items.view")]
public async Task<IActionResult> GetAll(
[FromQuery] Guid? categoryId,
[FromQuery] Guid? subCategoryId,
[FromQuery] Guid? brandId) =>
Ok(await service.GetAllAsync(categoryId, subCategoryId, brandId));
[HttpGet("{id:guid}")]
[Authorize(Policy = "items.view")]
public async Task<IActionResult> GetById(Guid id)
{
var result = await service.GetByIdAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
[Authorize(Policy = "items.create")]
public async Task<IActionResult> Create(CreateItemRequest request, CancellationToken ct)
{
try
{
var result = await service.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
catch (InvalidOperationException ex)
{
return Conflict(new { error = ex.Message });
}
}
[HttpPut("{id:guid}")]
[Authorize(Policy = "items.update")]
public async Task<IActionResult> Update(Guid id, UpdateItemRequest request, CancellationToken ct)
{
var result = await service.UpdateAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpDelete("{id:guid}")]
[Authorize(Policy = "items.delete")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var deleted = await service.DeleteAsync(id, ct);
return deleted ? NoContent() : NotFound();
}
[HttpPut("{id:guid}/uom-conversions")]
[Authorize(Policy = "items.update")]
public async Task<IActionResult> ReplaceConversions(Guid id, ReplaceConversionsRequest request, CancellationToken ct)
{
var result = await service.ReplaceConversionsAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
}
+63
View File
@@ -0,0 +1,63 @@
namespace Distribution.Features.Item;
public record UomConversionRequest(Guid FromUomId, Guid ToUomId, decimal Factor);
public record CreateItemRequest(
string Sku,
string Name,
string? Description,
Guid CategoryId,
Guid? SubCategoryId,
Guid? BrandId,
Guid BaseUomId,
decimal? SalePrice,
List<UomConversionRequest>? Conversions);
public record UpdateItemRequest(
string Name,
string? Description,
Guid CategoryId,
Guid? SubCategoryId,
Guid? BrandId,
Guid BaseUomId,
decimal? SalePrice);
public record ReplaceConversionsRequest(List<UomConversionRequest> Conversions);
public record UomConversionResponse(Guid Id, Guid FromUomId, string FromUomName, Guid ToUomId, string ToUomName, decimal Factor);
public record ItemSummaryResponse(
Guid Id,
string Sku,
string Name,
Guid CategoryId,
string CategoryName,
Guid? SubCategoryId,
string? SubCategoryName,
Guid? BrandId,
string? BrandName,
Guid BaseUomId,
string BaseUomName,
decimal? SalePrice,
Models.SyncSource SyncSource,
bool IsActive,
DateTime CreatedAt);
public record ItemDetailResponse(
Guid Id,
string Sku,
string Name,
string? Description,
Guid CategoryId,
string CategoryName,
Guid? SubCategoryId,
string? SubCategoryName,
Guid? BrandId,
string? BrandName,
Guid BaseUomId,
string BaseUomName,
decimal? SalePrice,
Models.SyncSource SyncSource,
bool IsActive,
DateTime CreatedAt,
List<UomConversionResponse> Conversions);
@@ -0,0 +1,64 @@
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Item;
public class ItemRepository(AppDBContext context) : IItemRepository
{
public async Task<Models.Item?> GetByIdAsync(Guid id) =>
await context.Items
.Include(i => i.Category)
.Include(i => i.SubCategory)
.Include(i => i.Brand)
.Include(i => i.BaseUom)
.Include(i => i.Conversions).ThenInclude(c => c.FromUom)
.Include(i => i.Conversions).ThenInclude(c => c.ToUom)
.FirstOrDefaultAsync(i => i.Id == id);
public async Task<List<Models.Item>> GetAllAsync(Guid? categoryId = null, Guid? subCategoryId = null, Guid? brandId = null)
{
var query = context.Items
.Include(i => i.Category)
.Include(i => i.SubCategory)
.Include(i => i.Brand)
.Include(i => i.BaseUom)
.Where(i => i.IsActive);
if (categoryId.HasValue) query = query.Where(i => i.FK_CategoryId == categoryId);
if (subCategoryId.HasValue) query = query.Where(i => i.FK_SubCategoryId == subCategoryId);
if (brandId.HasValue) query = query.Where(i => i.FK_BrandId == brandId);
return await query.ToListAsync();
}
public async Task<bool> SkuExistsAsync(string sku, Guid? excludeId = null)
{
var query = context.Items.Where(i => i.Sku == sku);
if (excludeId.HasValue) query = query.Where(i => i.Id != excludeId);
return await query.AnyAsync();
}
public async Task<Models.Item?> GetByExternalErpItemIdAsync(int erpId) =>
await context.Items
.Include(i => i.Conversions)
.FirstOrDefaultAsync(i => i.ExternalErpItemId == erpId);
public async Task AddAsync(Models.Item entity) =>
await context.Items.AddAsync(entity);
public void Update(Models.Item entity) =>
context.Items.Update(entity);
public void Remove(Models.Item entity)
{
entity.IsActive = false;
context.Items.Update(entity);
}
public async Task ReplaceConversionsAsync(Guid itemId, List<Models.UomConversion> conversions)
{
var existing = await context.UomConversions.Where(c => c.FK_ItemId == itemId).ToListAsync();
context.UomConversions.RemoveRange(existing);
await context.UomConversions.AddRangeAsync(conversions);
}
}
+126
View File
@@ -0,0 +1,126 @@
using Distribution.Infra.UoW;
namespace Distribution.Features.Item;
public class ItemService(IItemRepository repo, IUnitOfWork uow)
{
private static UomConversionResponse MapConversion(Models.UomConversion c) =>
new(c.Id, c.FK_FromUomId, c.FromUom.Name, c.FK_ToUomId, c.ToUom.Name, c.Factor);
private static ItemSummaryResponse MapSummary(Models.Item i) =>
new(i.Id, i.Sku, i.Name,
i.FK_CategoryId, i.Category.Name,
i.FK_SubCategoryId, i.SubCategory?.Name,
i.FK_BrandId, i.Brand?.Name,
i.FK_BaseUomId, i.BaseUom.Name,
i.SalePrice, i.SyncSource, i.IsActive, i.CreatedAt);
private static ItemDetailResponse MapDetail(Models.Item i) =>
new(i.Id, i.Sku, i.Name, i.Description,
i.FK_CategoryId, i.Category.Name,
i.FK_SubCategoryId, i.SubCategory?.Name,
i.FK_BrandId, i.Brand?.Name,
i.FK_BaseUomId, i.BaseUom.Name,
i.SalePrice, i.SyncSource, i.IsActive, i.CreatedAt,
i.Conversions.Select(MapConversion).ToList());
public async Task<List<ItemSummaryResponse>> GetAllAsync(Guid? categoryId = null, Guid? subCategoryId = null, Guid? brandId = null) =>
(await repo.GetAllAsync(categoryId, subCategoryId, brandId))
.Select(MapSummary)
.ToList();
public async Task<ItemDetailResponse?> GetByIdAsync(Guid id)
{
var item = await repo.GetByIdAsync(id);
if (item is null || !item.IsActive) return null;
return MapDetail(item);
}
public async Task<ItemDetailResponse> CreateAsync(CreateItemRequest request, CancellationToken ct = default)
{
if (await repo.SkuExistsAsync(request.Sku))
throw new InvalidOperationException($"SKU '{request.Sku}' already exists.");
var item = new Models.Item
{
Sku = request.Sku,
Name = request.Name,
Description = request.Description,
FK_CategoryId = request.CategoryId,
FK_SubCategoryId = request.SubCategoryId,
FK_BrandId = request.BrandId,
FK_BaseUomId = request.BaseUomId,
SalePrice = request.SalePrice,
SyncSource = Models.SyncSource.Manual
};
var conversions = (request.Conversions ?? []).Select(c => new Models.UomConversion
{
FK_ItemId = item.Id,
FK_FromUomId = c.FromUomId,
FK_ToUomId = c.ToUomId,
Factor = c.Factor
}).ToList();
item.Conversions = conversions;
await uow.BeginAsync(ct);
try { await repo.AddAsync(item); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return (await GetByIdAsync(item.Id))!;
}
public async Task<ItemDetailResponse?> UpdateAsync(Guid id, UpdateItemRequest request, CancellationToken ct = default)
{
var item = await repo.GetByIdAsync(id);
if (item is null || !item.IsActive) return null;
if (item.SyncSource == Models.SyncSource.Erp) return null;
item.Name = request.Name;
item.Description = request.Description;
item.FK_CategoryId = request.CategoryId;
item.FK_SubCategoryId = request.SubCategoryId;
item.FK_BrandId = request.BrandId;
item.FK_BaseUomId = request.BaseUomId;
item.SalePrice = request.SalePrice;
await uow.BeginAsync(ct);
try { repo.Update(item); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return (await GetByIdAsync(id))!;
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
var item = await repo.GetByIdAsync(id);
if (item is null || !item.IsActive) return false;
if (item.SyncSource == Models.SyncSource.Erp) return false;
await uow.BeginAsync(ct);
try { repo.Remove(item); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return true;
}
public async Task<List<UomConversionResponse>?> ReplaceConversionsAsync(Guid id, ReplaceConversionsRequest request, CancellationToken ct = default)
{
var item = await repo.GetByIdAsync(id);
if (item is null || !item.IsActive) return null;
if (item.SyncSource == Models.SyncSource.Erp) return null;
var conversions = request.Conversions.Select(c => new Models.UomConversion
{
FK_ItemId = id,
FK_FromUomId = c.FromUomId,
FK_ToUomId = c.ToUomId,
Factor = c.Factor
}).ToList();
await uow.BeginAsync(ct);
try { await repo.ReplaceConversionsAsync(id, conversions); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return (await GetByIdAsync(id))!.Conversions;
}
}