Fixes #21

Merged
DeepnaPooja merged 61 commits from fixes into dev 2026-09-10 16:00:04 +05:30
5 changed files with 91 additions and 8 deletions
Showing only changes of commit 6ba8d2fd19 - Show all commits
@@ -3,6 +3,7 @@ namespace Distribution.Features.SupplierClaims;
public interface ISupplierClaimRepository
{
Task<Models.SupplierClaim?> GetByIdAsync(Guid id);
Task<List<Models.SupplierClaim>> GetAllAsync();
Task<List<Models.SupplierClaim>> GetByPurchaseOrderIdAsync(Guid purchaseOrderId);
Task AddAsync(Models.SupplierClaim entity);
void Update(Models.SupplierClaim entity);
@@ -6,6 +6,10 @@ namespace Distribution.Features.SupplierClaims;
[ApiController]
public class SupplierClaimController(SupplierClaimService service, ICurrentUserService currentUser) : ControllerBase
{
[HttpGet("api/supplier-claims")]
public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync());
[HttpGet("api/supplier-claims/{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
@@ -20,22 +24,37 @@ public class SupplierClaimController(SupplierClaimService service, ICurrentUserS
[HttpPost("api/purchase-orders/{id:guid}/claims")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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(); }
@@ -6,6 +6,15 @@ public record SupplierClaimItemRequest(Guid ItemId, decimal Quantity, SupplierCl
public record CreateSupplierClaimRequest(
Guid? GoodsReceiptId,
string Details,
string? EvidenceUrl,
List<SupplierClaimItemRequest> 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<SupplierClaimItemRequest> 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,
@@ -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<List<Models.SupplierClaim>> GetAllAsync() =>
await context.SupplierClaims
.Include(c => c.Items).ThenInclude(i => i.Item)
.Where(c => c.IsActive)
.OrderByDescending(c => c.CreatedAt)
.ToListAsync();
public async Task<List<Models.SupplierClaim>> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) =>
await context.SupplierClaims
.Include(c => c.Items).ThenInclude(i => i.Item)
@@ -26,6 +26,9 @@ public class SupplierClaimService(
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();
@@ -83,6 +86,44 @@ public class SupplierClaimService(
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)