diff --git a/Distribution/Features/Returns/IReturnRepository.cs b/Distribution/Features/Returns/IReturnRepository.cs index 3840cda..efd2bce 100644 --- a/Distribution/Features/Returns/IReturnRepository.cs +++ b/Distribution/Features/Returns/IReturnRepository.cs @@ -3,8 +3,10 @@ namespace Distribution.Features.Returns; public interface IReturnRepository { Task GetByIdAsync(Guid id); + Task> GetAllAsync(); Task> GetByOrderIdAsync(Guid orderId); Task AddAsync(Models.SaleReturn entity); + void Update(Models.SaleReturn entity); Task AddCreditNoteAsync(Models.CreditNote entity); /// Whether this store has ever had this item on any of its (active) orders. diff --git a/Distribution/Features/Returns/ReturnController.cs b/Distribution/Features/Returns/ReturnController.cs index 4abc660..4612ed5 100644 --- a/Distribution/Features/Returns/ReturnController.cs +++ b/Distribution/Features/Returns/ReturnController.cs @@ -6,6 +6,10 @@ namespace Distribution.Features.Returns; [ApiController] public class ReturnController(ReturnService service, ICurrentUserService currentUser) : ControllerBase { + [HttpGet("api/returns")] + public async Task GetAll() => + Ok(await service.GetAllAsync()); + [HttpGet("api/returns/{id:guid}")] public async Task GetById(Guid id) { @@ -13,6 +17,17 @@ public class ReturnController(ReturnService service, ICurrentUserService current return result is null ? NotFound() : Ok(result); } + [HttpPatch("api/returns/{id:guid}/reasons")] + public async Task 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")] public async Task GetByOrder(Guid id) => Ok(await service.GetByOrderIdAsync(id)); @@ -20,10 +35,13 @@ public class ReturnController(ReturnService service, ICurrentUserService current [HttpPost("api/orders/{id:guid}/returns")] public async Task 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 { - 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 }); } diff --git a/Distribution/Features/Returns/ReturnDTOs.cs b/Distribution/Features/Returns/ReturnDTOs.cs index da3534d..834ca98 100644 --- a/Distribution/Features/Returns/ReturnDTOs.cs +++ b/Distribution/Features/Returns/ReturnDTOs.cs @@ -4,7 +4,17 @@ namespace Distribution.Features.Returns; public record ReturnItemRequest(Guid ItemId, decimal Quantity, ReturnReasonCode ReasonCode); -public record CreateSaleReturnRequest(ReturnMode Mode, List Items); +public record CreateSaleReturnRequest( + ReturnMode Mode, + List 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 Items); public record SaleReturnItemResponse( Guid Id, diff --git a/Distribution/Features/Returns/ReturnRepository.cs b/Distribution/Features/Returns/ReturnRepository.cs index 7d2cf04..ee553f5 100644 --- a/Distribution/Features/Returns/ReturnRepository.cs +++ b/Distribution/Features/Returns/ReturnRepository.cs @@ -11,6 +11,14 @@ public class ReturnRepository(AppDBContext context) : IReturnRepository .Include(r => r.Items).ThenInclude(i => i.Item) .FirstOrDefaultAsync(r => r.Id == id); + public async Task> 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> GetByOrderIdAsync(Guid orderId) => await context.SaleReturns .Include(r => r.Order) @@ -22,6 +30,9 @@ public class ReturnRepository(AppDBContext context) : IReturnRepository public async Task AddAsync(Models.SaleReturn entity) => await context.SaleReturns.AddAsync(entity); + public void Update(Models.SaleReturn entity) => + context.SaleReturns.Update(entity); + public async Task AddCreditNoteAsync(Models.CreditNote entity) => await context.CreditNotes.AddAsync(entity); diff --git a/Distribution/Features/Returns/ReturnService.cs b/Distribution/Features/Returns/ReturnService.cs index 4f7b9e4..731bc18 100644 --- a/Distribution/Features/Returns/ReturnService.cs +++ b/Distribution/Features/Returns/ReturnService.cs @@ -31,6 +31,33 @@ public class ReturnService( return Map(r); } + public async Task> GetAllAsync() => + (await repo.GetAllAsync()).Select(Map).ToList(); + + /// + /// Quantities/mode are immutable once created (the stock/order effect already happened) — + /// only each line's reason code can be corrected after the fact. + /// + public async Task 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> GetByOrderIdAsync(Guid orderId) => (await repo.GetByOrderIdAsync(orderId)).Select(Map).ToList();