using Distribution.Features.Grn; using Distribution.Features.Po; using Distribution.Infra.UoW; using Distribution.Models; namespace Distribution.Features.SupplierClaims; public class SupplierClaimService( ISupplierClaimRepository repo, IPoRepository poRepo, IGrnRepository grnRepo, IUnitOfWork uow) { private static SupplierClaimItemResponse MapItem(SupplierClaimItem i) => new(i.Id, i.FK_ItemId, i.Item.Sku, i.Item.Name, i.Quantity, i.IssueType); private static SupplierClaimResponse Map(SupplierClaim c) => new(c.Id, c.FK_PurchaseOrderId, c.FK_GoodsReceiptId, c.Details, c.EvidenceUrl, c.Status, c.ResolutionType, c.ResolutionAmount, c.ResolutionNotes, c.ResolvedAt, c.IsActive, c.CreatedAt, c.Items.Where(i => i.IsActive).Select(MapItem).ToList()); public async Task GetByIdAsync(Guid id) { var c = await repo.GetByIdAsync(id); if (c is null || !c.IsActive) return null; return Map(c); } public async Task> GetAllAsync() => (await repo.GetAllAsync()).Select(Map).ToList(); public async Task> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) => (await repo.GetByPurchaseOrderIdAsync(purchaseOrderId)).Select(Map).ToList(); public async Task CreateAsync(Guid purchaseOrderId, CreateSupplierClaimRequest request, Guid raisedByUserId, CancellationToken ct = default) { var po = await poRepo.GetByIdAsync(purchaseOrderId); if (po is null || !po.IsActive) throw new InvalidOperationException("Purchase order not found."); if (string.IsNullOrWhiteSpace(request.Details)) throw new InvalidOperationException("Claim details are required."); if (request.Items is null || request.Items.Count == 0) throw new InvalidOperationException("A supplier claim must have at least one line item."); if (request.GoodsReceiptId is not null) { var grn = await grnRepo.GetByIdAsync(request.GoodsReceiptId.Value) ?? throw new InvalidOperationException("Goods receipt not found."); if (grn.FK_PurchaseOrderId != purchaseOrderId) throw new InvalidOperationException("Goods receipt does not belong to this purchase order."); } var claim = new SupplierClaim { FK_PurchaseOrderId = purchaseOrderId, FK_GoodsReceiptId = request.GoodsReceiptId, Details = request.Details, EvidenceUrl = request.EvidenceUrl, RaisedByUserId = raisedByUserId }; foreach (var line in request.Items) { if (line.Quantity <= 0) throw new InvalidOperationException("Claim quantity must be greater than zero."); claim.Items.Add(new SupplierClaimItem { FK_ItemId = line.ItemId, Quantity = line.Quantity, IssueType = line.IssueType }); } await uow.BeginAsync(ct); try { await repo.AddAsync(claim); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } var saved = await repo.GetByIdAsync(claim.Id); return Map(saved!); } public async Task UpdateAsync(Guid id, UpdateSupplierClaimRequest request, CancellationToken ct = default) { var claim = await repo.GetByIdAsync(id) ?? throw new InvalidOperationException("Supplier claim not found."); if (claim.Status != SupplierClaimStatus.Open) throw new InvalidOperationException("Only an open claim can be edited."); if (string.IsNullOrWhiteSpace(request.Details)) throw new InvalidOperationException("Claim details are required."); if (request.Items is null || request.Items.Count == 0) throw new InvalidOperationException("A supplier claim must have at least one line item."); claim.Details = request.Details; claim.EvidenceUrl = request.EvidenceUrl; foreach (var existing in claim.Items.Where(i => i.IsActive)) existing.IsActive = false; foreach (var line in request.Items) { if (line.Quantity <= 0) throw new InvalidOperationException("Claim quantity must be greater than zero."); claim.Items.Add(new SupplierClaimItem { FK_ItemId = line.ItemId, Quantity = line.Quantity, IssueType = line.IssueType }); } await uow.BeginAsync(ct); try { repo.Update(claim); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } var saved = await repo.GetByIdAsync(claim.Id); return Map(saved!); } public async Task ApproveAsync(Guid id, ApproveSupplierClaimRequest request, Guid approverId, CancellationToken ct = default) { var claim = await repo.GetByIdAsync(id) ?? throw new KeyNotFoundException("Supplier claim not found."); if (claim.Status != SupplierClaimStatus.Open) throw new InvalidOperationException("Only an open claim can be approved."); if (request.ResolutionType != SupplierClaimResolutionType.Replacement && request.ResolutionAmount is null) throw new InvalidOperationException("ResolutionAmount is required for Credit, Refund, and PriceAdjustment."); claim.Status = SupplierClaimStatus.Approved; claim.ResolutionType = request.ResolutionType; claim.ResolutionAmount = request.ResolutionType == SupplierClaimResolutionType.Replacement ? null : request.ResolutionAmount; claim.ResolutionNotes = request.ResolutionNotes; claim.ResolvedByUserId = approverId; claim.ResolvedAt = DateTime.Now; await uow.BeginAsync(ct); try { repo.Update(claim); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return Map(claim); } public async Task RejectAsync(Guid id, RejectSupplierClaimRequest request, Guid approverId, CancellationToken ct = default) { var claim = await repo.GetByIdAsync(id) ?? throw new KeyNotFoundException("Supplier claim not found."); if (claim.Status != SupplierClaimStatus.Open) throw new InvalidOperationException("Only an open claim can be rejected."); claim.Status = SupplierClaimStatus.Rejected; claim.ResolutionNotes = request.ResolutionNotes; claim.ResolvedByUserId = approverId; claim.ResolvedAt = DateTime.Now; await uow.BeginAsync(ct); try { repo.Update(claim); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return Map(claim); } }