178 lines
6.9 KiB
C#
178 lines
6.9 KiB
C#
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<SupplierClaimResponse?> GetByIdAsync(Guid id)
|
|
{
|
|
var c = await repo.GetByIdAsync(id);
|
|
if (c is null || !c.IsActive) return null;
|
|
return Map(c);
|
|
}
|
|
|
|
public async Task<List<SupplierClaimResponse>> GetAllAsync() =>
|
|
(await repo.GetAllAsync()).Select(Map).ToList();
|
|
|
|
public async Task<List<SupplierClaimResponse>> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) =>
|
|
(await repo.GetByPurchaseOrderIdAsync(purchaseOrderId)).Select(Map).ToList();
|
|
|
|
public async Task<SupplierClaimResponse> 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<SupplierClaimResponse> 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<SupplierClaimResponse> 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<SupplierClaimResponse> 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);
|
|
}
|
|
}
|