shoc-backend/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs
Alexandre Brandizzi 7d245eb717
refactor: enforce backend boundaries and optimize dispatch (#30)
* refactor(api): enforce service and data-service boundaries

* refactor(api): complete feature service boundaries

* refactor(identity): enforce service and data boundaries

* refactor(vendors): enforce service and data boundaries

* refactor(workorders): enforce service and data boundaries

* refactor(backend): enforce architecture and optimize dispatch

* style(backend): format changed architecture files

* fix(architecture): address backend review follow-ups

* fix(backend): sanitize exception disclosure in changed API endpoints

Replace raw exception-message disclosure (ex.Message) returned to API
callers with a stable sanitized public message plus correlated structured
internal logging, across the endpoints changed in this PR.

- Add SanitizedErrors helper: logs the original exception at Error with a
  generated correlation id and returns a stable public message referencing
  it so support can trace without exposing internals.
- Inject ILogger<T> into the 14 changed controllers and route every
  ex.Message/dbex.Message disclosure through the helper, preserving status
  codes, response shapes, and business data (e.g. OpenWorkOrders).
- Leave FluentValidation (vex.Errors) and existing fixed-message catches
  untouched; out-of-scope controllers (Account/Contact/Employee/Asset/
  PMSchedule) are unchanged.
- Add focused tests proving internal exception text is not returned and
  that Error logging carrying the original exception is invoked.

* fix(architecture): abstract job run state access

* style: format board update service

* test: use collection assertion idiom
2026-07-24 17:35:34 -03:00

346 lines
13 KiB
C#

using Api.SeaHavenIndustries.DTOs;
using Api.SeaHavenIndustries.Helper;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SeaHaven.DataServices.Models;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/WorkOrder")]
[Route("api/workorders")]
public class WorkOrderController : Controller
{
private readonly IWorkOrderService _workOrderService;
private readonly ILogger<WorkOrderController> _logger;
public WorkOrderController(
IWorkOrderService workOrderService,
ILogger<WorkOrderController> logger)
{
_workOrderService = workOrderService;
_logger = logger;
}
[HttpPost]
[Route("AddWorkorder")]
public async Task<IActionResult> Addworkorder([FromForm] Workorder_DTO model)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var input = new CreateWorkOrderWithDetailsInput
{
WorkerOrderTitle = model.WorkerOrderTitle ?? "",
Description = model.Description,
Priority = model.Priority?.ToString(),
LocationId = model.LocationId,
AssignTo = model.AssignTo,
DueDate = model.DueDate,
ContactIds = model.ContactIds,
CategoryIds = model.CategoryIds,
Attachments = model.Attachments,
BeforPhotoAttachment = model.BeforPhotoAttachment,
AfterPhotoAttachment = model.AfterPhotoAttachment,
SignOffAttachment = model.SignOffAttachment
};
var workOrderId = await _workOrderService.CreateWorkOrderWithDetailsAsync(input, userId!);
return Ok(new DataResponse { Message = "Work order created successfully", Status = "200", Data = workOrderId });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpPost]
[HttpPut("{id:int}")]
[Route("EditWorkorder")]
public async Task<IActionResult> Editworkorder([FromForm] EditWorkorder_DTO model)
{
try
{
if (!model.Id.HasValue)
return BadRequest(new Response { Status = "Error", Message = "Work order ID is required" });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var input = new UpdateWorkOrderWithDetailsInput
{
Id = model.Id.Value,
WorkerOrderTitle = model.WorkerOrderTitle,
Description = model.Description,
Status = model.Status?.ToString(),
Priority = model.Priority?.ToString(),
LocationId = model.LocationId,
AssignTo = model.AssignTo,
DueDate = model.DueDate,
CompletedDate = model.CompletedDate,
ContactIds = model.ContactIds,
CategoryIds = model.CategoryIds,
Attachments = model.Attachments,
BeforPhotoAttachment = model.BeforPhotoAttachment,
AfterPhotoAttachment = model.AfterPhotoAttachment,
SignOffAttachment = model.SignOffAttachment
};
var success = await _workOrderService.UpdateWorkOrderWithDetailsAsync(input, userId!);
if (!success)
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpGet("GetWorkOrderList")]
public async Task<IActionResult> GetWorkOrderList(string? search = "", int page = 1, int pageSize = 12, string? sortBy = "lastUpdated", string? sortDir = "desc", string? status = "", string? assignee = "", int? locationId = null)
{
var (data, totalCount) = await _workOrderService.GetWorkOrderListPagedAsync(
page, pageSize, search, status, assignee, locationId, sortBy, sortDir);
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
};
return Ok(viewModel);
}
[Authorize(Roles = "Admin")]
[HttpDelete]
[Route("DeleteWorkorder")]
public async Task<IActionResult> Deleteworkorder([FromBody] EditWorkorder_DTO model)
{
int id = model.Id.HasValue ? model.Id.Value : 0;
if (id == 0)
return BadRequest("Id is required");
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var success = await _workOrderService.DeleteWorkOrderCascadeAsync(id, userId!);
if (!success)
return BadRequest("Work order not found!");
return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpGet]
[Route("GetFilteredWorkorder")]
public async Task<IActionResult> GetFilteredWorkorder(
[FromQuery] string[] assignto,
[FromQuery] int[]? location,
[FromQuery] string[]? priority,
[FromQuery] string[]? status,
[FromQuery] string[]? duedate,
[FromQuery] string search = "",
[FromQuery] string sort = "Sort Order",
[FromQuery] string sortby = "Sort By",
int page = 1,
int pageSize = 10)
{
try
{
var result = await _workOrderService.GetFilteredWorkOrdersAsync(
assignto, location, priority, status, duedate, search, sort, sortby, page, pageSize);
return Ok(result);
}
catch (Exception)
{
return StatusCode(500, "Internal Server Error");
}
}
[HttpGet("{id:int}")]
[HttpGet("GetWorkorderById")]
public async Task<IActionResult> GetWorkorderById(int id)
{
var detail = await _workOrderService.GetWorkOrderDetailAsync(id);
if (detail == null)
return BadRequest(new Response { Status = "Error", Message = "ID not found!" });
return Ok(detail);
}
[HttpGet]
[Route("GetFilteredWorkorder2")]
public async Task<IActionResult> GetFilteredWorkorder2(
[FromQuery] string[] assignto,
[FromQuery] int[]? location,
[FromQuery] string[]? priority,
[FromQuery] string[]? status,
[FromQuery] string[]? duedate,
[FromQuery] string search = "",
[FromQuery] string sort = "Sort Order",
[FromQuery] string sortby = "Sort By",
int page = 1,
int pageSize = 10)
{
try
{
var result = await _workOrderService.GetFilteredWorkOrders2Async(
assignto, location, priority, status, duedate, search, sort, sortby, page, pageSize);
return Ok(result);
}
catch (Exception)
{
var totalCount = await _workOrderService.GetTotalWorkOrderCountAsync();
return Ok(new Pagination_DTO
{
Data = Array.Empty<FilteredWorkOrderReadModel>(),
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
});
}
}
[HttpPost]
[Route("ChangeStatus")]
public async Task<IActionResult> ChangeStatus(int id, string status)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var result = await _workOrderService.ChangeStatusAsync(id, status, userId!);
if (result == null)
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = result });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpPost]
[Route("ChangeAssignment")]
public async Task<IActionResult> ChangeAssignment(int id, string? assignTo)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var result = await _workOrderService.ChangeAssignmentAsync(id, assignTo, userId!);
if (result == null)
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpPost]
[Route("AddComment")]
public async Task<IActionResult> AddComment([FromForm] Comments_DTO model)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var input = new AddCommentInput
{
WorkorderId = model.WorkorderId,
Text = model.Text,
CommentType = model.CommentType,
Document = model.Document
};
var saved = await _workOrderService.AddCommentAsync(input, userId!);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = saved });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpPost]
[Route("AddCommentJson")]
public async Task<IActionResult> AddCommentJson([FromBody] Comments_DTO model)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var input = new AddCommentInput
{
WorkorderId = model.WorkorderId,
Text = model.Text,
CommentType = model.CommentType
};
var result = await _workOrderService.AddCommentJsonAsync(input, userId!);
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpGet]
[Route("GetComments")]
public async Task<IActionResult> GetComments()
{
var data = await _workOrderService.GetCommentsAsync();
return Ok(data);
}
[HttpGet]
[Route("GetCommentsByWorkorderId")]
public async Task<IActionResult> GetCommentsByWorkorderId(int woid)
{
var data = await _workOrderService.GetCommentsByWorkorderIdAsync(woid);
return Ok(data);
}
[HttpGet]
[Route("GetworkordersDD")]
public async Task<IActionResult> GetworkordersDD()
{
var data = await _workOrderService.GetWorkordersDDAsync();
return Ok(data);
}
[HttpGet]
[Route("Getworkorders")]
public async Task<IActionResult> Getworkorders()
{
var data = await _workOrderService.GetWorkordersAsync();
return Ok(data);
}
}
}