shoc-backend/Api.SeaHavenIndustries/Controllers/WorkOrderDispatchController.cs

215 lines
7.8 KiB
C#
Raw Normal View History

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
using Api.SeaHavenIndustries.DTOs;
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.Exceptions;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/WorkOrder")]
[Route("api/workorders")]
public class WorkOrderDispatchController : Controller
{
private readonly IWorkOrderDispatchService _dispatchService;
private readonly ILogger<WorkOrderDispatchController> _logger;
public WorkOrderDispatchController(
IWorkOrderDispatchService dispatchService,
ILogger<WorkOrderDispatchController> logger)
{
_dispatchService = dispatchService;
_logger = logger;
}
[HttpPost]
[Route("DispatchToVendor")]
public async Task<IActionResult> DispatchToVendor([FromBody] Dispatch_DTO model, CancellationToken cancellationToken)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var input = new DispatchToVendorInput
{
WorkOrderIds = model.WorkOrderIds,
VendorIds = model.VendorIds,
NTEAmount = model.NTEAmount,
Description = model.Description,
ScheduledDate = model.ScheduledDate,
TaskListTemplateId = model.TaskListTemplateId,
CustomChecklistItems = model.CustomChecklistItems,
UserId = userId
};
var result = await _dispatchService.DispatchToVendorAsync(input, cancellationToken);
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpGet("GetDispatches/{workOrderId}")]
public async Task<IActionResult> GetDispatches(int workOrderId)
{
var data = await _dispatchService.GetDispatchesAsync(workOrderId);
return Ok(data);
}
[HttpGet("GetDispatch/{id}")]
public async Task<IActionResult> GetDispatchById(int id)
{
var d = await _dispatchService.GetDispatchDetailAsync(id);
if (d == null)
return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
return Ok(d);
}
[HttpPost("UpdateDispatch")]
public async Task<IActionResult> UpdateDispatch([FromBody] UpdateDispatch_DTO model)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var input = new UpdateDispatchInput
{
Id = model.Id,
Status = model.Status,
NTEAmount = model.NTEAmount,
ScheduledDate = model.ScheduledDate,
CompletedDate = model.CompletedDate,
Description = model.Description,
UserId = userId
};
var success = await _dispatchService.UpdateDispatchAsync(input);
if (!success)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
return Ok(new { message = "Dispatch updated" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpPost("AddDispatchComment")]
public async Task<IActionResult> AddDispatchComment([FromBody] DispatchComment_DTO model)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var input = new AddDispatchCommentInput
{
DispatchId = model.DispatchId,
Text = model.Text,
SendEmail = model.SendEmail,
UserId = userId
};
var result = await _dispatchService.AddDispatchCommentAsync(input);
if (result == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpPost("VerifyDispatch")]
public async Task<IActionResult> VerifyDispatch(int dispatchId)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var result = await _dispatchService.VerifyDispatchAsync(dispatchId, userId!);
if (result == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
return Ok(result);
}
catch (DispatchVerificationException ex)
{
return BadRequest(new { status = "Error", message = "Cannot verify", missing = ex.Missing });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpPost("AddDispatchSignoff")]
public async Task<IActionResult> AddDispatchSignoff([FromBody] DispatchSignoff_DTO model)
{
try
{
var input = new AddDispatchSignoffInput
{
DispatchId = model.DispatchId,
SignoffType = model.SignoffType,
Name = model.Name,
Signature = model.Signature,
SignatureMethod = model.SignatureMethod
};
var result = await _dispatchService.AddDispatchSignoffAsync(input);
if (result == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
return Ok(result);
}
catch (InvalidOperationException ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex, "The dispatch signoff could not be added") });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) });
}
}
[HttpPost("UpdateChecklistItem")]
public async Task<IActionResult> UpdateChecklistItem([FromBody] ChecklistItemUpdate_DTO model)
{
var input = new ChecklistItemUpdateInput
{
Id = model.Id,
IsCompleted = model.IsCompleted,
CompletedBy = model.CompletedBy
};
var result = await _dispatchService.UpdateChecklistItemAsync(input);
if (result == null)
return NotFound(new Response { Status = "Error", Message = "Checklist item not found" });
return Ok(result);
}
[HttpPost("AddChecklistItem")]
public async Task<IActionResult> AddChecklistItem([FromBody] AddChecklistItem_DTO model)
{
var input = new AddChecklistItemInput
{
DispatchId = model.DispatchId,
WorkOrderId = model.WorkOrderId,
ItemText = model.ItemText
};
var result = await _dispatchService.AddChecklistItemAsync(input);
return Ok(result);
}
}
}