Add GetAll and UpdateReasons endpoints to ReturnController
This commit is contained in:
@@ -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 }); }
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user