Add GetAll and UpdateReasons endpoints to ReturnController

This commit is contained in:
2026-09-08 16:08:07 +05:30
parent 81d92e16bb
commit e267f94d46
5 changed files with 71 additions and 3 deletions
@@ -3,8 +3,10 @@ namespace Distribution.Features.Returns;
public interface IReturnRepository public interface IReturnRepository
{ {
Task<Models.SaleReturn?> GetByIdAsync(Guid id); Task<Models.SaleReturn?> GetByIdAsync(Guid id);
Task<List<Models.SaleReturn>> GetAllAsync();
Task<List<Models.SaleReturn>> GetByOrderIdAsync(Guid orderId); Task<List<Models.SaleReturn>> GetByOrderIdAsync(Guid orderId);
Task AddAsync(Models.SaleReturn entity); Task AddAsync(Models.SaleReturn entity);
void Update(Models.SaleReturn entity);
Task AddCreditNoteAsync(Models.CreditNote entity); Task AddCreditNoteAsync(Models.CreditNote entity);
/// <summary>Whether this store has ever had this item on any of its (active) orders.</summary> /// <summary>Whether this store has ever had this item on any of its (active) orders.</summary>
@@ -6,6 +6,10 @@ namespace Distribution.Features.Returns;
[ApiController] [ApiController]
public class ReturnController(ReturnService service, ICurrentUserService currentUser) : ControllerBase public class ReturnController(ReturnService service, ICurrentUserService currentUser) : ControllerBase
{ {
[HttpGet("api/returns")]
public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync());
[HttpGet("api/returns/{id:guid}")] [HttpGet("api/returns/{id:guid}")]
public async Task<IActionResult> GetById(Guid id) public async Task<IActionResult> GetById(Guid id)
{ {
@@ -13,6 +17,17 @@ public class ReturnController(ReturnService service, ICurrentUserService current
return result is null ? NotFound() : Ok(result); return result is null ? NotFound() : Ok(result);
} }
[HttpPatch("api/returns/{id:guid}/reasons")]
public async Task<IActionResult> UpdateReasons(Guid id, UpdateSaleReturnReasonsRequest request, CancellationToken ct)
{
try
{
var result = await service.UpdateReasonsAsync(id, request, ct);
return Ok(result);
}
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
}
[HttpGet("api/orders/{id:guid}/returns")] [HttpGet("api/orders/{id:guid}/returns")]
public async Task<IActionResult> GetByOrder(Guid id) => public async Task<IActionResult> GetByOrder(Guid id) =>
Ok(await service.GetByOrderIdAsync(id)); Ok(await service.GetByOrderIdAsync(id));
@@ -20,10 +35,13 @@ public class ReturnController(ReturnService service, ICurrentUserService current
[HttpPost("api/orders/{id:guid}/returns")] [HttpPost("api/orders/{id:guid}/returns")]
public async Task<IActionResult> Create(Guid id, CreateSaleReturnRequest request, CancellationToken ct) public async Task<IActionResult> Create(Guid id, CreateSaleReturnRequest 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.CreatedByUserId;
if (userId is null) return BadRequest(new { error = "CreatedByUserId is required while auth is unavailable" });
try 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); return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
} }
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
+11 -1
View File
@@ -4,7 +4,17 @@ namespace Distribution.Features.Returns;
public record ReturnItemRequest(Guid ItemId, decimal Quantity, ReturnReasonCode ReasonCode); public record ReturnItemRequest(Guid ItemId, decimal Quantity, ReturnReasonCode ReasonCode);
public record CreateSaleReturnRequest(ReturnMode Mode, List<ReturnItemRequest> Items); public record CreateSaleReturnRequest(
ReturnMode Mode,
List<ReturnItemRequest> Items,
// TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28.
Guid? CreatedByUserId = null);
// The return's financial/stock effect (credit, order total, vehicle stock credit) is applied
// immediately on create with no separate approval step, so quantities/mode can never be edited
// after the fact without desyncing stock and order totals. Only each line's reason code is editable.
public record UpdateReturnReasonItem(Guid ItemId, ReturnReasonCode ReasonCode);
public record UpdateSaleReturnReasonsRequest(List<UpdateReturnReasonItem> Items);
public record SaleReturnItemResponse( public record SaleReturnItemResponse(
Guid Id, Guid Id,
@@ -11,6 +11,14 @@ public class ReturnRepository(AppDBContext context) : IReturnRepository
.Include(r => r.Items).ThenInclude(i => i.Item) .Include(r => r.Items).ThenInclude(i => i.Item)
.FirstOrDefaultAsync(r => r.Id == id); .FirstOrDefaultAsync(r => r.Id == id);
public async Task<List<Models.SaleReturn>> GetAllAsync() =>
await context.SaleReturns
.Include(r => r.Order)
.Include(r => r.Items).ThenInclude(i => i.Item)
.Where(r => r.IsActive)
.OrderByDescending(r => r.CreatedAt)
.ToListAsync();
public async Task<List<Models.SaleReturn>> GetByOrderIdAsync(Guid orderId) => public async Task<List<Models.SaleReturn>> GetByOrderIdAsync(Guid orderId) =>
await context.SaleReturns await context.SaleReturns
.Include(r => r.Order) .Include(r => r.Order)
@@ -22,6 +30,9 @@ public class ReturnRepository(AppDBContext context) : IReturnRepository
public async Task AddAsync(Models.SaleReturn entity) => public async Task AddAsync(Models.SaleReturn entity) =>
await context.SaleReturns.AddAsync(entity); await context.SaleReturns.AddAsync(entity);
public void Update(Models.SaleReturn entity) =>
context.SaleReturns.Update(entity);
public async Task AddCreditNoteAsync(Models.CreditNote entity) => public async Task AddCreditNoteAsync(Models.CreditNote entity) =>
await context.CreditNotes.AddAsync(entity); await context.CreditNotes.AddAsync(entity);
@@ -31,6 +31,33 @@ public class ReturnService(
return Map(r); return Map(r);
} }
public async Task<List<SaleReturnResponse>> GetAllAsync() =>
(await repo.GetAllAsync()).Select(Map).ToList();
/// <summary>
/// Quantities/mode are immutable once created (the stock/order effect already happened) —
/// only each line's reason code can be corrected after the fact.
/// </summary>
public async Task<SaleReturnResponse> UpdateReasonsAsync(Guid id, UpdateSaleReturnReasonsRequest request, CancellationToken ct = default)
{
var r = await repo.GetByIdAsync(id);
if (r is null || !r.IsActive)
throw new InvalidOperationException("Return not found.");
foreach (var line in request.Items)
{
var item = r.Items.FirstOrDefault(i => i.FK_ItemId == line.ItemId && i.IsActive)
?? throw new InvalidOperationException($"Item {line.ItemId} not found on this return.");
item.ReasonCode = line.ReasonCode;
}
await uow.BeginAsync(ct);
try { repo.Update(r); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return Map(r);
}
public async Task<List<SaleReturnResponse>> GetByOrderIdAsync(Guid orderId) => public async Task<List<SaleReturnResponse>> GetByOrderIdAsync(Guid orderId) =>
(await repo.GetByOrderIdAsync(orderId)).Select(Map).ToList(); (await repo.GetByOrderIdAsync(orderId)).Select(Map).ToList();