diff --git a/Distribution/Features/SupplierClaims/ISupplierClaimRepository.cs b/Distribution/Features/SupplierClaims/ISupplierClaimRepository.cs index 3bdcc97..bd1e8fe 100644 --- a/Distribution/Features/SupplierClaims/ISupplierClaimRepository.cs +++ b/Distribution/Features/SupplierClaims/ISupplierClaimRepository.cs @@ -3,6 +3,7 @@ namespace Distribution.Features.SupplierClaims; public interface ISupplierClaimRepository { Task GetByIdAsync(Guid id); + Task> GetAllAsync(); Task> GetByPurchaseOrderIdAsync(Guid purchaseOrderId); Task AddAsync(Models.SupplierClaim entity); void Update(Models.SupplierClaim entity); diff --git a/Distribution/Features/SupplierClaims/SupplierClaimController.cs b/Distribution/Features/SupplierClaims/SupplierClaimController.cs index 6c96880..d678f42 100644 --- a/Distribution/Features/SupplierClaims/SupplierClaimController.cs +++ b/Distribution/Features/SupplierClaims/SupplierClaimController.cs @@ -6,6 +6,10 @@ namespace Distribution.Features.SupplierClaims; [ApiController] public class SupplierClaimController(SupplierClaimService service, ICurrentUserService currentUser) : ControllerBase { + [HttpGet("api/supplier-claims")] + public async Task GetAll() => + Ok(await service.GetAllAsync()); + [HttpGet("api/supplier-claims/{id:guid}")] public async Task GetById(Guid id) { @@ -20,22 +24,37 @@ public class SupplierClaimController(SupplierClaimService service, ICurrentUserS [HttpPost("api/purchase-orders/{id:guid}/claims")] public async Task Create(Guid id, CreateSupplierClaimRequest request, CancellationToken ct) { - var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + // TEMPORARY: currentUser.UserId is always unset due to the broken auth middleware + // (testbug.md #28) — fall back to the explicitly supplied user until that's fixed. + var userId = currentUser.UserId ?? request.RaisedByUserId; + if (userId is null) return BadRequest(new { error = "RaisedByUserId is required while auth is unavailable" }); try { - var result = await service.CreateAsync(id, request, userId, ct); + var result = await service.CreateAsync(id, request, userId.Value, ct); return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); } catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } } + [HttpPut("api/supplier-claims/{id:guid}")] + public async Task Update(Guid id, UpdateSupplierClaimRequest request, CancellationToken ct) + { + try + { + var result = await service.UpdateAsync(id, request, ct); + return Ok(result); + } + catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } + } + [HttpPost("api/supplier-claims/{id:guid}/approve")] public async Task Approve(Guid id, ApproveSupplierClaimRequest request, CancellationToken ct) { - var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + var userId = currentUser.UserId ?? request.ApprovedByUserId; + if (userId is null) return BadRequest(new { error = "ApprovedByUserId is required while auth is unavailable" }); try { - var result = await service.ApproveAsync(id, request, userId, ct); + var result = await service.ApproveAsync(id, request, userId.Value, ct); return Ok(result); } catch (KeyNotFoundException) { return NotFound(); } @@ -45,10 +64,11 @@ public class SupplierClaimController(SupplierClaimService service, ICurrentUserS [HttpPost("api/supplier-claims/{id:guid}/reject")] public async Task Reject(Guid id, RejectSupplierClaimRequest request, CancellationToken ct) { - var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + var userId = currentUser.UserId ?? request.RejectedByUserId; + if (userId is null) return BadRequest(new { error = "RejectedByUserId is required while auth is unavailable" }); try { - var result = await service.RejectAsync(id, request, userId, ct); + var result = await service.RejectAsync(id, request, userId.Value, ct); return Ok(result); } catch (KeyNotFoundException) { return NotFound(); } diff --git a/Distribution/Features/SupplierClaims/SupplierClaimDTOs.cs b/Distribution/Features/SupplierClaims/SupplierClaimDTOs.cs index 0d4efd1..887379f 100644 --- a/Distribution/Features/SupplierClaims/SupplierClaimDTOs.cs +++ b/Distribution/Features/SupplierClaims/SupplierClaimDTOs.cs @@ -6,6 +6,15 @@ public record SupplierClaimItemRequest(Guid ItemId, decimal Quantity, SupplierCl public record CreateSupplierClaimRequest( Guid? GoodsReceiptId, + string Details, + string? EvidenceUrl, + List Items, + // TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28. + Guid? RaisedByUserId = null); + +// Only editable while the claim is still Open — Approve/Reject resolve it, and a resolved +// claim's details/items shouldn't silently change after the fact. +public record UpdateSupplierClaimRequest( string Details, string? EvidenceUrl, List Items); @@ -13,9 +22,14 @@ public record CreateSupplierClaimRequest( public record ApproveSupplierClaimRequest( SupplierClaimResolutionType ResolutionType, decimal? ResolutionAmount, - string? ResolutionNotes); + string? ResolutionNotes, + // TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28. + Guid? ApprovedByUserId = null); -public record RejectSupplierClaimRequest(string? ResolutionNotes); +public record RejectSupplierClaimRequest( + string? ResolutionNotes, + // TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28. + Guid? RejectedByUserId = null); public record SupplierClaimItemResponse( Guid Id, diff --git a/Distribution/Features/SupplierClaims/SupplierClaimRepository.cs b/Distribution/Features/SupplierClaims/SupplierClaimRepository.cs index 6fbacce..86d4d76 100644 --- a/Distribution/Features/SupplierClaims/SupplierClaimRepository.cs +++ b/Distribution/Features/SupplierClaims/SupplierClaimRepository.cs @@ -10,6 +10,13 @@ public class SupplierClaimRepository(AppDBContext context) : ISupplierClaimRepos .Include(c => c.Items).ThenInclude(i => i.Item) .FirstOrDefaultAsync(c => c.Id == id); + public async Task> GetAllAsync() => + await context.SupplierClaims + .Include(c => c.Items).ThenInclude(i => i.Item) + .Where(c => c.IsActive) + .OrderByDescending(c => c.CreatedAt) + .ToListAsync(); + public async Task> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) => await context.SupplierClaims .Include(c => c.Items).ThenInclude(i => i.Item) diff --git a/Distribution/Features/SupplierClaims/SupplierClaimService.cs b/Distribution/Features/SupplierClaims/SupplierClaimService.cs index 2f927bf..64b1b28 100644 --- a/Distribution/Features/SupplierClaims/SupplierClaimService.cs +++ b/Distribution/Features/SupplierClaims/SupplierClaimService.cs @@ -26,6 +26,9 @@ public class SupplierClaimService( 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(); @@ -83,6 +86,44 @@ public class SupplierClaimService( 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)