using Api.SeaHavenIndustries.Helper; using Data.SeaHavenIndustries; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; namespace Api.SeaHavenIndustries.Controllers { [Authorize] [ApiController] [Route("api/[controller]")] [Route("api/uplifts")] public class UpliftController : Controller { private readonly IUpliftService _upliftService; private readonly ILogger _logger; public UpliftController(IUpliftService upliftService, ILogger logger) { _upliftService = upliftService; _logger = logger; } [HttpGet] public async Task List([FromQuery] string? status = "Pending", [FromQuery] int? tier = null, [FromQuery] int page = 1, [FromQuery] int pageSize = 25, CancellationToken cancellationToken = default) { var result = await _upliftService.ListAsync(User, status, tier, page, pageSize, cancellationToken); return Ok(new DataResponse { Status = "Success", Data = result }); } [HttpGet("dispatch/{dispatchId:int}")] public async Task ListForDispatch(int dispatchId, CancellationToken cancellationToken = default) { var items = await _upliftService.ListForDispatchAsync(User, dispatchId, cancellationToken); return Ok(new DataResponse { Status = "Success", Data = items }); } [HttpPost("{id:int}/approve")] public async Task Approve(int id, [FromBody] DecisionRequest? body, CancellationToken cancellationToken = default) { try { var result = await _upliftService.ApproveAsync(User, id, body?.Note, cancellationToken); return Ok(new DataResponse { Status = "Success", Data = result }); } catch (KeyNotFoundException ex) { return NotFound(new Response { Status = "Error", Message = _logger.Sanitize(ex, "Uplift request not found") }); } catch (UpliftForbiddenException ex) { return StatusCode(403, new Response { Status = "Error", Message = _logger.Sanitize(ex, "You are not authorized to perform this action on this uplift request") }); } catch (InvalidOperationException ex) { return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex, "This uplift request cannot be modified in its current state") }); } } [HttpPost("{id:int}/deny")] public async Task Deny(int id, [FromBody] DecisionRequest? body, CancellationToken cancellationToken = default) { try { var result = await _upliftService.DenyAsync(User, id, body?.Note, cancellationToken); return Ok(new DataResponse { Status = "Success", Data = result }); } catch (KeyNotFoundException ex) { return NotFound(new Response { Status = "Error", Message = _logger.Sanitize(ex, "Uplift request not found") }); } catch (UpliftForbiddenException ex) { return StatusCode(403, new Response { Status = "Error", Message = _logger.Sanitize(ex, "You are not authorized to perform this action on this uplift request") }); } catch (InvalidOperationException ex) { return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex, "This uplift request cannot be modified in its current state") }); } } // SH-101: canonical reject route (result is Rejected); deny alias stays compatible. [HttpPost("{id:int}/reject")] public async Task Reject(int id, [FromBody] DecisionRequest? body, CancellationToken cancellationToken = default) { try { var result = await _upliftService.RejectAsync(User, id, body?.Note, cancellationToken); return Ok(new DataResponse { Status = "Success", Data = result }); } catch (KeyNotFoundException ex) { return NotFound(new Response { Status = "Error", Message = _logger.Sanitize(ex, "Uplift request not found") }); } catch (UpliftForbiddenException ex) { return StatusCode(403, new Response { Status = "Error", Message = _logger.Sanitize(ex, "You are not authorized to perform this action on this uplift request") }); } catch (InvalidOperationException ex) { return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex, "This uplift request cannot be modified in its current state") }); } } // SH-101: internal request-changes route (note + tier authorization + audit). [HttpPost("{id:int}/request-changes")] public async Task RequestChanges(int id, [FromBody] DecisionRequest? body, CancellationToken cancellationToken = default) { try { var result = await _upliftService.RequestChangesAsync(User, id, body?.Note ?? string.Empty, cancellationToken); return Ok(new DataResponse { Status = "Success", Data = result }); } catch (KeyNotFoundException ex) { return NotFound(new Response { Status = "Error", Message = _logger.Sanitize(ex, "Uplift request not found") }); } catch (UpliftForbiddenException ex) { return StatusCode(403, new Response { Status = "Error", Message = _logger.Sanitize(ex, "You are not authorized to perform this action on this uplift request") }); } catch (InvalidOperationException ex) { return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex, "This uplift request cannot be modified in its current state") }); } } [HttpPost("{id:int}/revoke")] public async Task Revoke( int id, [FromBody] DecisionRequest? body, CancellationToken cancellationToken = default) { try { var result = await _upliftService.RevokeAsync( User, id, body?.Note ?? string.Empty, cancellationToken); return Ok(new DataResponse { Status = "Success", Data = result }); } catch (KeyNotFoundException ex) { return NotFound(new Response { Status = "Error", Message = _logger.Sanitize(ex, "Uplift request not found") }); } catch (UpliftForbiddenException ex) { return StatusCode(403, new Response { Status = "Error", Message = _logger.Sanitize(ex, "You are not authorized to revoke this uplift") }); } catch (InvalidOperationException ex) { return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex, "This uplift request cannot be revoked") }); } } [HttpGet("can-approve")] public IActionResult CanApprove([FromQuery] int tier) { return Ok(new DataResponse { Status = "Success", Data = new { canApprove = _upliftService.CanApprove(User, tier) } }); } // SH-101: authorized internal download of the Passed UpliftEvidence file linked to // a specific uplift request. Server-side linkage only; no vendor/public path or // document id is accepted from the client. 404 covers missing request/evidence and // any non-UpliftEvidence document; 423 covers a scan that has not Passed. [HttpGet("{id:int}/evidence")] public async Task DownloadEvidence(int id, CancellationToken cancellationToken = default) { try { var result = await _upliftService.GetEvidenceForDownloadAsync(User, id, cancellationToken); return result.Outcome switch { VendorDocumentDownloadOutcome.Ok => File(result.Content!, result.ContentType!, result.FileName!), VendorDocumentDownloadOutcome.Locked => StatusCode(StatusCodes.Status423Locked, new Response { Status = "Locked", Message = "The uplift evidence is not available for download yet." }), _ => NotFound(new Response { Status = "Error", Message = "Uplift evidence not found" }) }; } catch (UpliftForbiddenException ex) { return StatusCode(403, new Response { Status = "Error", Message = _logger.Sanitize(ex, "You are not authorized to view evidence for this uplift request") }); } } public class DecisionRequest { public string? Note { get; set; } } } }