Refactor/main flow #37
@@ -5,8 +5,8 @@ namespace Distribution.Features.ShopPO;
|
||||
public interface IShopPORepository
|
||||
{
|
||||
Task<Models.ShopPO?> GetByIdAsync(Guid id);
|
||||
Task<List<Models.ShopPO>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, bool? confirmed, int page, int pageSize);
|
||||
Task<int> GetCountAsync(Guid? agentId, Guid? routeId, Guid? shopId, bool? confirmed);
|
||||
Task<List<Models.ShopPO>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? regionId, Guid? shopId, bool? confirmed, ShopPOStatus? status, int page, int pageSize);
|
||||
Task<int> GetCountAsync(Guid? agentId, Guid? routeId, Guid? regionId, Guid? shopId, bool? confirmed, ShopPOStatus? status);
|
||||
Task<List<Models.ShopPO>> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId);
|
||||
Task<List<Models.ShopPOItem>> GetActiveItemsByShopPOIdAsync(Guid shopPOId);
|
||||
Task<List<Models.ShopPO>> GetByStoreIdAsync(Guid storeId);
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.ShopPO;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/shop-pos")]
|
||||
public class ShopPOController(ShopPOService service) : ControllerBase
|
||||
public class ShopPOController(ShopPOService service, ICurrentUserService currentUser) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll(
|
||||
[FromQuery] Guid? agentId,
|
||||
[FromQuery] Guid? routeId,
|
||||
[FromQuery] Guid? regionId,
|
||||
[FromQuery] Guid? shopId,
|
||||
[FromQuery] bool? confirmed,
|
||||
[FromQuery] ShopPOStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
{
|
||||
var result = await service.GetAllAsync(agentId, routeId, shopId, confirmed, page, pageSize);
|
||||
var result = await service.GetAllAsync(agentId, routeId, regionId, shopId, confirmed, status, page, pageSize);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
@@ -25,4 +29,66 @@ public class ShopPOController(ShopPOService service) : ControllerBase
|
||||
var result = await service.GetByIdAsync(id);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(CreateShopPORequest request, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||
try
|
||||
{
|
||||
var result = await service.CreateAsync(request, userId, ct);
|
||||
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/items")]
|
||||
public async Task<IActionResult> UpdateItems(Guid id, UpdateShopPOItemsRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UpdateItemsAsync(id, request, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/confirm")]
|
||||
public async Task<IActionResult> Confirm(Guid id, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||
try
|
||||
{
|
||||
var result = await service.ConfirmAsync(id, userId, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/confirm-and-create-invoice")]
|
||||
public async Task<IActionResult> ConfirmAndCreateInvoice(Guid id, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||
try
|
||||
{
|
||||
var result = await service.ConfirmAndCreateInvoiceAsync(id, userId, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/cancel")]
|
||||
public async Task<IActionResult> Cancel(Guid id, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.CancelAsync(id, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,27 +13,32 @@ public class ShopPORepository(AppDBContext context) : IShopPORepository
|
||||
.ThenInclude(i => i.Item)
|
||||
.FirstOrDefaultAsync(p => p.Id == id && p.IsActive);
|
||||
|
||||
public async Task<List<Models.ShopPO>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, bool? confirmed, int page, int pageSize) =>
|
||||
public async Task<List<Models.ShopPO>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? regionId, Guid? shopId, bool? confirmed, ShopPOStatus? status, int page, int pageSize) =>
|
||||
await context.ShopPOs
|
||||
.Include(p => p.Store)
|
||||
.Include(p => p.Store).ThenInclude(s => s.Route)
|
||||
.Include(p => p.Items.Where(i => i.IsActive)).ThenInclude(i => i.Item)
|
||||
.Where(p => p.IsActive
|
||||
&& (agentId == null || p.RaisedById == agentId)
|
||||
&& (routeId == null || p.Store.FK_RouteId == routeId)
|
||||
&& (regionId == null || p.Store.Route.FK_RegionId == regionId)
|
||||
&& (shopId == null || p.FK_StoreId == shopId)
|
||||
&& (confirmed == null || (confirmed == true ? p.ConfirmedAt != null : p.ConfirmedAt == null)))
|
||||
&& (confirmed == null || (confirmed == true ? p.ConfirmedAt != null : p.ConfirmedAt == null))
|
||||
&& (status == null || p.Status == status))
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> GetCountAsync(Guid? agentId, Guid? routeId, Guid? shopId, bool? confirmed) =>
|
||||
public async Task<int> GetCountAsync(Guid? agentId, Guid? routeId, Guid? regionId, Guid? shopId, bool? confirmed, ShopPOStatus? status) =>
|
||||
await context.ShopPOs
|
||||
.Include(p => p.Store)
|
||||
.Include(p => p.Store).ThenInclude(s => s.Route)
|
||||
.CountAsync(p => p.IsActive
|
||||
&& (agentId == null || p.RaisedById == agentId)
|
||||
&& (routeId == null || p.Store.FK_RouteId == routeId)
|
||||
&& (regionId == null || p.Store.Route.FK_RegionId == regionId)
|
||||
&& (shopId == null || p.FK_StoreId == shopId)
|
||||
&& (confirmed == null || (confirmed == true ? p.ConfirmedAt != null : p.ConfirmedAt == null)));
|
||||
&& (confirmed == null || (confirmed == true ? p.ConfirmedAt != null : p.ConfirmedAt == null))
|
||||
&& (status == null || p.Status == status));
|
||||
|
||||
public async Task<List<Models.ShopPO>> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId) =>
|
||||
await context.ShopPOs
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Features.Invoice;
|
||||
using Distribution.Features.PriceList;
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
|
||||
@@ -6,6 +8,8 @@ namespace Distribution.Features.ShopPO;
|
||||
|
||||
public class ShopPOService(
|
||||
IShopPORepository repo,
|
||||
IInvoiceRepository invoiceRepo,
|
||||
PriceListService priceListService,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
private static ShopPOResponse ToResponse(Models.ShopPO p) =>
|
||||
@@ -17,10 +21,10 @@ public class ShopPOService(
|
||||
i.Quantity, i.QuantityFulfilled, i.UnitPrice, i.IsActive)).ToList(),
|
||||
p.IsActive, p.CreatedAt);
|
||||
|
||||
public async Task<PagedResponse<ShopPOResponse>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, bool? confirmed, int page, int pageSize)
|
||||
public async Task<PagedResponse<ShopPOResponse>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? regionId, Guid? shopId, bool? confirmed, ShopPOStatus? status, int page, int pageSize)
|
||||
{
|
||||
var total = await repo.GetCountAsync(agentId, routeId, shopId, confirmed);
|
||||
var items = await repo.GetAllAsync(agentId, routeId, shopId, confirmed, page, pageSize);
|
||||
var total = await repo.GetCountAsync(agentId, routeId, regionId, shopId, confirmed, status);
|
||||
var items = await repo.GetAllAsync(agentId, routeId, regionId, shopId, confirmed, status, page, pageSize);
|
||||
return new PagedResponse<ShopPOResponse>(page, pageSize, total, items.Select(ToResponse).ToList());
|
||||
}
|
||||
|
||||
@@ -29,4 +33,174 @@ public class ShopPOService(
|
||||
var po = await repo.GetByIdAsync(id);
|
||||
return po is null ? null : ToResponse(po);
|
||||
}
|
||||
|
||||
public async Task<ShopPOResponse> CreateAsync(CreateShopPORequest request, Guid raisedById, CancellationToken ct = default)
|
||||
{
|
||||
if (request.Items.Count == 0)
|
||||
throw new InvalidOperationException("At least one item is required");
|
||||
|
||||
var items = new List<ShopPOItem>();
|
||||
decimal total = 0;
|
||||
|
||||
foreach (var line in request.Items)
|
||||
{
|
||||
var unitPrice = line.UnitPrice
|
||||
?? (await priceListService.ResolveByStoreAsync(line.ItemId, request.StoreId, DateTime.Now))?.Price
|
||||
?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}");
|
||||
|
||||
items.Add(new ShopPOItem
|
||||
{
|
||||
FK_ItemId = line.ItemId,
|
||||
Quantity = line.Quantity,
|
||||
QuantityFulfilled = 0,
|
||||
UnitPrice = unitPrice
|
||||
});
|
||||
total += line.Quantity * unitPrice;
|
||||
}
|
||||
|
||||
var po = new Models.ShopPO
|
||||
{
|
||||
FK_StoreId = request.StoreId,
|
||||
RaisedById = raisedById,
|
||||
TotalAmount = total,
|
||||
Status = ShopPOStatus.Pending
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await repo.AddAsync(po);
|
||||
foreach (var item in items) item.FK_ShopPOId = po.Id;
|
||||
await repo.AddItemsAsync(items);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
var saved = await repo.GetByIdAsync(po.Id);
|
||||
return ToResponse(saved!);
|
||||
}
|
||||
|
||||
public async Task<ShopPOResponse> UpdateItemsAsync(Guid id, UpdateShopPOItemsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var po = await repo.GetByIdAsync(id)
|
||||
?? throw new KeyNotFoundException("Shop PO not found");
|
||||
|
||||
if (po.ConfirmedAt is not null)
|
||||
throw new InvalidOperationException("Items can only be changed before the Shop PO is confirmed");
|
||||
|
||||
if (request.Items.Count == 0)
|
||||
throw new InvalidOperationException("At least one item is required");
|
||||
|
||||
var newItems = new List<ShopPOItem>();
|
||||
decimal total = 0;
|
||||
|
||||
foreach (var line in request.Items)
|
||||
{
|
||||
var unitPrice = line.UnitPrice
|
||||
?? (await priceListService.ResolveByStoreAsync(line.ItemId, po.FK_StoreId, DateTime.Now))?.Price
|
||||
?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}");
|
||||
|
||||
newItems.Add(new ShopPOItem
|
||||
{
|
||||
FK_ShopPOId = id,
|
||||
FK_ItemId = line.ItemId,
|
||||
Quantity = line.Quantity,
|
||||
QuantityFulfilled = 0,
|
||||
UnitPrice = unitPrice
|
||||
});
|
||||
total += line.Quantity * unitPrice;
|
||||
}
|
||||
|
||||
po.TotalAmount = total;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await repo.ReplaceItemsAsync(id, newItems);
|
||||
repo.Update(po);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
var saved = await repo.GetByIdAsync(id);
|
||||
return ToResponse(saved!);
|
||||
}
|
||||
|
||||
public async Task<ShopPOResponse> ConfirmAsync(Guid id, Guid confirmedBy, CancellationToken ct = default)
|
||||
{
|
||||
var po = await repo.GetByIdAsync(id)
|
||||
?? throw new KeyNotFoundException("Shop PO not found");
|
||||
|
||||
if (po.ConfirmedAt is not null)
|
||||
throw new InvalidOperationException("Shop PO is already confirmed");
|
||||
|
||||
if (po.Status == ShopPOStatus.Cancelled)
|
||||
throw new InvalidOperationException("Cannot confirm a cancelled Shop PO");
|
||||
|
||||
po.ConfirmedAt = DateTime.Now;
|
||||
po.ConfirmedBy = confirmedBy;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(po); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse(po);
|
||||
}
|
||||
|
||||
public async Task<ShopPOResponse> ConfirmAndCreateInvoiceAsync(Guid id, Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var po = await repo.GetByIdAsync(id)
|
||||
?? throw new KeyNotFoundException("Shop PO not found");
|
||||
|
||||
if (po.ConfirmedAt is not null)
|
||||
throw new InvalidOperationException("Shop PO is already confirmed");
|
||||
|
||||
if (po.Status == ShopPOStatus.Cancelled)
|
||||
throw new InvalidOperationException("Cannot confirm a cancelled Shop PO");
|
||||
|
||||
po.ConfirmedAt = DateTime.Now;
|
||||
po.ConfirmedBy = userId;
|
||||
|
||||
var invoice = new Models.Invoice
|
||||
{
|
||||
FK_ShopPOId = po.Id,
|
||||
Status = InvoiceStatus.Draft,
|
||||
PaymentStatus = PaymentStatus.Pending,
|
||||
CreatedBy = userId,
|
||||
TotalAmount = po.TotalAmount
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await invoiceRepo.AddAsync(invoice);
|
||||
po.InvoiceId = invoice.Id;
|
||||
repo.Update(po);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
var saved = await repo.GetByIdAsync(id);
|
||||
return ToResponse(saved!);
|
||||
}
|
||||
|
||||
public async Task<ShopPOResponse> CancelAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var po = await repo.GetByIdAsync(id)
|
||||
?? throw new KeyNotFoundException("Shop PO not found");
|
||||
|
||||
if (po.Status == ShopPOStatus.Cancelled)
|
||||
throw new InvalidOperationException("Shop PO is already cancelled");
|
||||
|
||||
if (po.Status is ShopPOStatus.PartiallyFulfilled or ShopPOStatus.Fulfilled)
|
||||
throw new InvalidOperationException($"Cannot cancel a Shop PO in {po.Status} status");
|
||||
|
||||
po.Status = ShopPOStatus.Cancelled;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(po); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse(po);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user