Files
ERP_Distribution/Distribution/Features/ShopPO/ShopPOService.cs
T
2026-09-29 17:07:16 +05:30

237 lines
9.1 KiB
C#

using Distribution.Features.Invoice;
using Distribution.Features.PriceList;
using Distribution.Features.Stock;
using Distribution.Infra.UoW;
using Distribution.Models;
namespace Distribution.Features.ShopPO;
public class ShopPOService(
IShopPORepository repo,
IInvoiceRepository invoiceRepo,
PriceListService priceListService,
IUnitOfWork uow)
{
private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary<Guid, (string Name, string Role)>? users = null) =>
new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty,
p.Store?.StoreAddress, p.Store?.ContactNumber,
p.RaisedById, p.InvoiceId, p.TotalAmount, p.Status,
p.ConfirmedAt, p.ConfirmedBy, p.FulfilledAt,
p.Items.Where(i => i.IsActive).Select(i =>
new ShopPOItemResponse(i.Id, i.FK_ItemId, i.Item?.Name ?? string.Empty,
i.Quantity, i.QuantityFulfilled, i.UnitPrice, i.IsActive)).ToList(),
p.IsActive, p.CreatedAt,
users != null && users.TryGetValue(p.RaisedById, out var u) ? u.Name : null,
users != null && users.TryGetValue(p.RaisedById, out var r) ? r.Role : null,
users != null && p.ConfirmedBy is Guid cb && users.TryGetValue(cb, out var c) ? c.Name : null,
users != null && p.ConfirmedBy is Guid cb2 && users.TryGetValue(cb2, out var c2) ? c2.Role : null,
p.CancelledBy, p.CancelledAt, p.CancelReason,
users != null && p.CancelledBy is Guid xb && users.TryGetValue(xb, out var x) ? x.Name : null,
users != null && p.CancelledBy is Guid xb2 && users.TryGetValue(xb2, out var x2) ? x2.Role : null);
private static IEnumerable<Guid> UserIds(Models.ShopPO p)
{
yield return p.RaisedById;
if (p.ConfirmedBy is Guid cb) yield return cb;
if (p.CancelledBy is Guid xb) yield return xb;
}
private async Task<ShopPOResponse> ToResponseWithUserAsync(Models.ShopPO p) =>
ToResponse(p, await repo.GetUserInfoAsync(UserIds(p)));
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, regionId, shopId, confirmed, status);
var items = await repo.GetAllAsync(agentId, routeId, regionId, shopId, confirmed, status, page, pageSize);
var users = await repo.GetUserInfoAsync(items.SelectMany(UserIds));
return new PagedResponse<ShopPOResponse>(page, pageSize, total, items.Select(p => ToResponse(p, users)).ToList());
}
public async Task<ShopPOResponse?> GetByIdAsync(Guid id)
{
var po = await repo.GetByIdAsync(id);
if (po is null) return null;
var users = await repo.GetUserInfoAsync(UserIds(po));
return ToResponse(po, users);
}
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 await ToResponseWithUserAsync(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.Status == ShopPOStatus.Cancelled)
throw new InvalidOperationException("Cannot edit a cancelled Shop PO");
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 await ToResponseWithUserAsync(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 await ToResponseWithUserAsync(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 await ToResponseWithUserAsync(saved!);
}
public async Task<ShopPOResponse> CancelAsync(Guid id, Guid cancelledBy, string reason, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(reason))
throw new InvalidOperationException("A cancellation reason is required");
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;
po.CancelledAt = DateTime.Now;
po.CancelledBy = cancelledBy;
po.CancelReason = reason.Trim();
await uow.BeginAsync(ct);
try { repo.Update(po); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return await ToResponseWithUserAsync(po);
}
}