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? 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 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 ToResponseWithUserAsync(Models.ShopPO p) => ToResponse(p, await repo.GetUserInfoAsync(UserIds(p))); public async Task> 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(page, pageSize, total, items.Select(p => ToResponse(p, users)).ToList()); } public async Task 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> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId) { var items = await repo.GetConfirmedWithoutInvoiceAsync(agentId, routeId, shopId); var users = await repo.GetUserInfoAsync(items.SelectMany(UserIds)); return items.Select(p => ToResponse(p, users)).ToList(); } public async Task 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(); 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 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(); 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 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 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 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); } }