item mng with sync
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user