using Distribution.Infra.Authorization; using Distribution.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Stock; [ApiController] [Route("api/stock")] public class StockController(StockService service, ICurrentUserService currentUser) : ControllerBase { // ── Query ────────────────────────────────────────────────────────────────── [HttpGet("balances")] [Authorize("stock.view")] public async Task GetBalances( [FromQuery] Guid? locationId, [FromQuery] LocationType? locationType, [FromQuery] Guid? itemId, [FromQuery] int page = 1, [FromQuery] int pageSize = 20) { var result = await service.GetBalancesAsync(locationId, locationType, itemId, page, pageSize); return Ok(result); } [HttpGet("low-stock")] [Authorize("stock.view")] public async Task GetLowStock([FromQuery] Guid? locationId) { var result = await service.GetLowStockAsync(locationId); return Ok(result); } [HttpGet("balances/{itemId}/total")] [Authorize("stock.view")] public async Task GetItemTotal(Guid itemId) { var result = await service.GetItemTotalAsync(itemId); if (result is null) return NotFound(); return Ok(result); } /// Next free number for auto-generated serials (e.g. after ...-0005 the answer is 6). [HttpGet("serials/next-sequence")] [Authorize("stock.view")] public async Task GetNextSerialSequence([FromQuery] Guid itemId, [FromQuery] string prefix = "") { var next = await service.GetNextSerialSequenceAsync(itemId, prefix); return Ok(new { next }); } [HttpGet("movements")] //[Authorize("stock.view")] public async Task GetMovements( [FromQuery] Guid? locationId, [FromQuery] Guid? itemId, [FromQuery] MovementType? type, [FromQuery] DateTime? from, [FromQuery] DateTime? to, [FromQuery] Guid? referenceId, [FromQuery] Guid? batchId, [FromQuery] Guid? serialId, [FromQuery] Guid? createdBy, [FromQuery] string? reasonCode, [FromQuery] int page = 1, [FromQuery] int pageSize = 20) { var filter = new MovementFilter(locationId, itemId, type, from, to, referenceId, batchId, serialId, createdBy, reasonCode); var result = await service.GetMovementsAsync(filter, page, pageSize); return Ok(result); } /// Opening / in / out / closing quantity for one item at one location over a date range. [HttpGet("movements/summary")] public async Task GetMovementSummary( [FromQuery] Guid itemId, [FromQuery] Guid locationId, [FromQuery] DateTime from, [FromQuery] DateTime to) { var result = await service.GetMovementSummaryAsync(itemId, locationId, from, to); return Ok(result); } // ── Write ────────────────────────────────────────────────────────────────── [HttpPost("transfer")] [Authorize("stock.manage")] public async Task Transfer(TransferRequest request, CancellationToken ct) { if (currentUser.UserId is null) return Unauthorized(); try { var result = await service.TransferAsync(request, currentUser.UserId.Value, ct); return Ok(result); } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } [HttpPost("damage")] [Authorize("stock.manage")] public async Task RecordDamage(DamageRequest request, CancellationToken ct) { if (currentUser.UserId is null) return Unauthorized(); try { var result = await service.RecordDamageAsync(request, currentUser.UserId.Value, ct); return Ok(result); } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } [HttpPost("customer-return")] [Authorize("stock.manage")] public async Task RecordCustomerReturn(CustomerReturnRequest request, CancellationToken ct) { if (currentUser.UserId is null) return Unauthorized(); try { var result = await service.RecordCustomerReturnAsync(request, currentUser.UserId.Value, ct); return Ok(result); } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } [HttpPost("adjustment")] [Authorize("stock.manage")] public async Task Adjust(AdjustmentRequest request, CancellationToken ct) { // 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.AdjustedByUserId; if (userId is null) return BadRequest(new { error = "AdjustedByUserId is required while auth is unavailable" }); try { var result = await service.AdjustAsync(request, userId.Value, ct); return Ok(result); } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } [HttpPatch("movements/{id}/reason")] [Authorize("stock.manage")] public async Task UpdateMovementReason(Guid id, UpdateMovementReasonRequest request, CancellationToken ct) { try { var result = await service.UpdateMovementReasonAsync(id, request, ct); return Ok(result); } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } }