shoc-backend/SeaHaven.DataServices/Implementation/WorkOrderDataService.cs

543 lines
22 KiB
C#
Raw Normal View History

2026-04-28 18:55:14 -05:00
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Helpers;
2026-04-28 18:55:14 -05:00
using SeaHaven.DataServices.Interfaces;
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 SeaHaven.DataServices.Models;
2026-04-28 18:55:14 -05:00
namespace SeaHaven.DataServices.Implementation
{
public class WorkOrderDataService : IWorkOrderDataService
{
private readonly ApplicationDbContext _context;
public WorkOrderDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task<WorkOrder?> GetByIdAsync(int id)
{
return await _context.workOrders.FindAsync(id);
}
public async Task<WorkOrder?> GetByIdWithDetailsAsync(int id)
{
return await _context.workOrders
.Include(w => w.Locations)
.Include(w => w.AssignToUser)
.FirstOrDefaultAsync(w => w.Id == id);
}
public async Task<WorkOrder?> GetByInternalNumberAsync(string internalNumber)
{
return await _context.workOrders
.FirstOrDefaultAsync(w => w.InternalWONumber == internalNumber);
}
public async Task<IEnumerable<WorkOrder>> GetAllAsync()
{
return await _context.workOrders.ToListAsync();
}
public async Task<IEnumerable<WorkOrder>> GetByLocationIdAsync(int locationId)
{
return await _context.workOrders
.Where(w => w.LocationId == locationId)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrder>> GetByAssigneeAsync(string assignee)
{
return await _context.workOrders
.Where(w => w.AssignTo == assignee)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrder>> GetByStatusAsync(string status)
{
return await _context.workOrders
.Where(w => w.Status == status)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrder>> GetByPriorityAsync(string priority)
{
return await _context.workOrders
.Where(w => w.Priority == priority)
.ToListAsync();
}
public async Task<(IEnumerable<WorkOrder> Items, int TotalCount)> GetPagedAsync(
2026-05-06 10:49:33 -05:00
int page,
int pageSize,
string? search = null,
string? status = null,
2026-04-28 18:55:14 -05:00
string? priority = null)
{
var query = _context.workOrders.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
2026-05-06 10:49:33 -05:00
query = query.Where(w =>
w.WorkerOrderNumber!.Contains(search) ||
2026-04-28 18:55:14 -05:00
w.WorkerOrderTitle!.Contains(search));
}
if (!string.IsNullOrWhiteSpace(status))
{
query = query.Where(w => w.Status == status);
}
if (!string.IsNullOrWhiteSpace(priority))
{
query = query.Where(w => w.Priority == priority);
}
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(w => w.CreatedDate)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
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
public async Task<(IEnumerable<WorkOrderListItemReadModel> Items, int TotalCount)> GetWorkOrderListPagedAsync(
2026-05-06 10:49:33 -05:00
int page,
int pageSize,
string? search = null,
string? status = null,
string? assignee = null,
int? locationId = null,
string? sortBy = "lastUpdated",
string? sortDir = "desc",
int? accountId = null)
2026-05-06 10:49:33 -05:00
{
search ??= "";
var query = _context.workOrders
.Include(w => w.Locations)
.Include(w => w.AssignToUser)
.Where(w => w.istemplate != true);
if (accountId.HasValue)
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
2026-05-06 10:49:33 -05:00
if (locationId.HasValue)
query = query.Where(w => w.LocationId == locationId.Value);
if (!string.IsNullOrWhiteSpace(status))
query = query.Where(w => w.Status == status);
if (assignee == "__unassigned")
query = query.Where(w => w.AssignTo == null || w.AssignTo == "");
else if (!string.IsNullOrWhiteSpace(assignee))
query = query.Where(w => w.AssignToUser != null && (w.AssignToUser.FirstName + " " + w.AssignToUser.LastName).Contains(assignee));
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(w =>
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
(w.InternalWONumber != null && w.InternalWONumber.Contains(search)) ||
(w.WorkerOrderNumber != null && w.WorkerOrderNumber.Contains(search)) ||
(w.WorkerOrderTitle != null && w.WorkerOrderTitle.Contains(search)) ||
(w.Locations != null && w.Locations.Title != null && w.Locations.Title.Contains(search)) ||
(w.Locations != null && w.Locations.Name != null && w.Locations.Name.Contains(search))
2026-05-06 10:49:33 -05:00
);
}
var totalCount = await query.CountAsync();
var isAsc = sortDir?.ToLower() == "asc";
IOrderedQueryable<WorkOrder> ordered = (sortBy?.ToLower()) switch
{
"number" => isAsc ? query.OrderBy(w => w.InternalWONumber) : query.OrderByDescending(w => w.InternalWONumber),
"title" => isAsc ? query.OrderBy(w => w.WorkerOrderTitle) : query.OrderByDescending(w => w.WorkerOrderTitle),
"location" => isAsc ? query.OrderBy(w => w.Locations != null ? w.Locations.Name : "") : query.OrderByDescending(w => w.Locations != null ? w.Locations.Name : ""),
"priority" => isAsc ? query.OrderBy(w => w.Priority) : query.OrderByDescending(w => w.Priority),
"status" => isAsc ? query.OrderBy(w => w.Status) : query.OrderByDescending(w => w.Status),
"createddate" => isAsc ? query.OrderBy(w => w.CreatedDate) : query.OrderByDescending(w => w.CreatedDate),
"assignedto" => isAsc ? query.OrderBy(w => w.AssignToUser != null ? w.AssignToUser.FirstName : "") : query.OrderByDescending(w => w.AssignToUser != null ? w.AssignToUser.FirstName : ""),
"lastupdated" => isAsc ? query.OrderBy(w => w.LastModificationTime) : query.OrderByDescending(w => w.LastModificationTime),
_ => query.OrderByDescending(w => w.Id)
};
var data = await ordered
.Skip((page - 1) * pageSize)
.Take(pageSize)
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
.Select(w => new WorkOrderListItemReadModel
2026-05-06 10:49:33 -05:00
{
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
Id = w.Id,
InternalWONumber = w.InternalWONumber,
WorkOrderNumber = w.WorkerOrderNumber,
Number = w.InternalWONumber ?? w.WorkerOrderNumber,
Title = w.WorkerOrderTitle,
Location = w.Locations != null ? w.Locations.Name : "",
LocationName = w.Locations != null ? w.Locations.Name : "",
Priority = w.Priority,
Status = w.Status,
CreatedDate = w.CreatedDate,
DueDate = w.DueDate,
AssignedTo = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
EmployeeName = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "",
LastUpdated = new[] {
2026-05-06 10:49:33 -05:00
w.LastModificationTime,
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
w.Comments!.Max(c => (DateTime?)c.CreatedDate),
2026-05-06 10:49:33 -05:00
_context.WorkOrderAuditLogs.Where(a => a.WorkOrderId == w.Id).Max(a => (DateTime?)a.CreatedAt)
}.Where(d => d.HasValue).Max()
})
.ToListAsync();
return (data, totalCount);
}
2026-04-28 18:55:14 -05:00
public async Task<WorkOrder> AddAsync(WorkOrder workOrder)
{
workOrder.CreatedDate ??= DateTime.UtcNow;
2026-04-28 18:55:14 -05:00
await _context.workOrders.AddAsync(workOrder);
await _context.SaveChangesAsync();
return workOrder;
}
public async Task UpdateAsync(WorkOrder workOrder)
{
workOrder.LastModificationTime = DateTime.UtcNow;
2026-04-28 18:55:14 -05:00
_context.workOrders.Update(workOrder);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.workOrders.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<IEnumerable<WorkOrderContacts>> GetWorkOrderContactsAsync(int workOrderId)
{
return await _context.WorkOrderContacts
.Where(wc => wc.WorkorderId == workOrderId)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrderCategories>> GetWorkOrderCategoriesAsync(int workOrderId)
{
return await _context.workOrderCategories
.Where(wc => wc.WorkorderId == workOrderId)
.ToListAsync();
}
public async Task<IEnumerable<WorkOrderAttachments>> GetWorkOrderAttachmentsAsync(int workOrderId)
{
return await _context.workOrderAttachments
.Where(wa => wa.WorkorderId == workOrderId)
.ToListAsync();
}
public async Task<IEnumerable<Comments>> GetWorkOrderCommentsAsync(int workOrderId)
{
return await _context.Comments
.Where(c => c.WorkerOrderId == workOrderId)
.ToListAsync();
}
2026-05-14 11:00:12 -05:00
public async Task<IEnumerable<WorkOrderAuditLog>> GetWorkOrderAuditLogsAsync(int workOrderId)
{
return await _context.WorkOrderAuditLogs
.Where(a => a.WorkOrderId == workOrderId)
.OrderByDescending(a => a.CreatedAt)
.ToListAsync();
}
public async Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable<int> contactIds)
{
var contacts = contactIds.Select(cid => new WorkOrderContacts
{
WorkorderId = workOrderId,
ContactId = cid
});
await _context.WorkOrderContacts.AddRangeAsync(contacts);
await _context.SaveChangesAsync();
}
public async Task AddWorkOrderCategoriesAsync(int workOrderId, IEnumerable<int> categoryIds)
{
var categories = categoryIds.Select(catId => new WorkOrderCategories
{
WorkorderId = workOrderId,
CategoryId = catId
});
await _context.workOrderCategories.AddRangeAsync(categories);
await _context.SaveChangesAsync();
}
public async Task AddWorkOrderAttachmentAsync(WorkOrderAttachments attachment)
{
await _context.workOrderAttachments.AddAsync(attachment);
await _context.SaveChangesAsync();
}
public async Task AddAuditLogAsync(WorkOrderAuditLog auditLog)
{
await _context.WorkOrderAuditLogs.AddAsync(auditLog);
await _context.SaveChangesAsync();
}
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
public async Task AddAuditLogsAsync(IReadOnlyCollection<WorkOrderAuditLog> auditLogs)
{
if (auditLogs.Count == 0)
return;
await _context.WorkOrderAuditLogs.AddRangeAsync(auditLogs);
await _context.SaveChangesAsync();
}
2026-05-14 11:00:12 -05:00
public async Task DeleteWorkOrderContactsAsync(int workOrderId)
{
var contacts = await _context.WorkOrderContacts
.Where(wc => wc.WorkorderId == workOrderId)
.ToListAsync();
if (contacts.Any())
{
_context.WorkOrderContacts.RemoveRange(contacts);
await _context.SaveChangesAsync();
}
}
public async Task DeleteWorkOrderCategoriesAsync(int workOrderId)
{
var categories = await _context.workOrderCategories
.Where(wc => wc.WorkorderId == workOrderId)
.ToListAsync();
if (categories.Any())
{
_context.workOrderCategories.RemoveRange(categories);
await _context.SaveChangesAsync();
}
}
public async Task DeleteWorkOrderAttachmentsAsync(int workOrderId)
{
var attachments = await _context.workOrderAttachments
.Where(wa => wa.WorkorderId == workOrderId)
.ToListAsync();
if (attachments.Any())
{
_context.workOrderAttachments.RemoveRange(attachments);
await _context.SaveChangesAsync();
}
}
2026-04-28 18:55:14 -05:00
public async Task<bool> ExistsAsync(int id)
{
return await _context.workOrders.AnyAsync(w => w.Id == id);
}
public async Task<bool> InternalNumberExistsAsync(string internalNumber, int? excludeId = null)
{
return await _context.workOrders.AnyAsync(w =>
w.InternalWONumber == internalNumber &&
(excludeId == null || w.Id != excludeId));
}
public async Task<string> GenerateNextInternalWONumberAsync()
{
return await Task.FromResult($"WO-{DateTime.Now:yyyyMMdd}-{new Random().Next(1000, 9999)}");
}
public async Task<int> CountAsync()
{
return await _context.workOrders.CountAsync();
}
public async Task<int> CountByStatusAsync(string status)
{
return await _context.workOrders.CountAsync(w => w.Status == status);
}
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
public async Task<int> CountByPriorityAsync(string priority)
{
return await _context.workOrders.CountAsync(w => w.Priority == priority);
}
2026-04-28 18:55:14 -05:00
public async Task<int> CountByLocationAsync(int locationId)
{
return await _context.workOrders.CountAsync(w => w.LocationId == locationId);
}
2026-05-14 11:00:12 -05:00
public async Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync(int? accountId = null)
2026-05-14 11:00:12 -05:00
{
IQueryable<WorkOrder> query = _context.workOrders
.Where(w => w.istemplate != true);
if (accountId.HasValue)
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
return await query
2026-05-14 11:00:12 -05:00
.Include(w => w.Locations)
.Include(w => w.AssignToUser)
.AsSplitQuery()
.ToListAsync();
}
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
public async Task<IReadOnlyList<WorkOrder>> GetByRequestedIdsWithDetailsAsync(
IReadOnlyCollection<int> ids,
CancellationToken cancellationToken)
{
var orderedIds = ids.Distinct().ToList();
if (orderedIds.Count == 0)
return Array.Empty<WorkOrder>();
var loaded = await _context.workOrders
.AsNoTracking()
.Where(w => orderedIds.Contains(w.Id) && w.istemplate != true)
.Include(w => w.Locations)
.ToListAsync(cancellationToken);
var byId = loaded.ToDictionary(w => w.Id);
var ordered = new List<WorkOrder>(orderedIds.Count);
foreach (var id in orderedIds)
{
if (byId.TryGetValue(id, out var workOrder))
ordered.Add(workOrder);
}
return ordered;
}
public async Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id, int? accountId = null)
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
{
var query = _context.workOrders
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
.AsNoTracking()
.Where(w => w.Id == id);
if (accountId.HasValue)
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
return await query
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
.Select(s => new WorkOrderDetailReadModel
{
Id = s.Id,
InternalWONumber = s.InternalWONumber,
WorkerOrderNumber = s.WorkerOrderNumber,
Title = s.WorkerOrderTitle,
Description = s.Description,
CreatedDate = s.CreatedDate,
DueDate = s.DueDate,
ScheduledDate = s.ScheduledDate,
CompletedDate = s.CompletedDate,
Status = s.Status,
Assignee = s.AssignToUser != null ? s.AssignToUser.FirstName : "",
Location = s.Locations != null ? s.Locations.Name : "",
LocationId = s.LocationId,
LocationZip = s.Locations != null ? (s.Locations.Zip ?? ExtractZip(s.Locations.Address1)) : "",
Priority = s.Priority,
Problem = s.Problem,
Trade = s.Trade,
SubTrade = s.SubTrade,
VendorNTE = s.VendorNTE,
Source = s.Source,
PO = s.PO,
TT = s.TT,
Poc = s.WorkOrderContacts!.Select(c => new ContactReadModel
{
Id = c.Id,
Name = c.POC != null ? (c.POC.FirstName + " " + c.POC.LastName).Trim() : "",
Email = c.POC != null ? c.POC.Email : "",
Phone = c.POC != null ? c.POC.PhoneNumber : ""
}).ToList(),
Category = s.WorkOrderCategories!.Select(c => new CategoryReadModel
{
Id = c.Id,
Name = c.Category != null ? c.Category.Name : ""
}).ToList(),
Attachments = s.workOrderAttachments!.Select(a => new AttachmentReadModel
{
Id = a.Id,
Attachment = a.Attachments
}).ToList(),
BeforPhotoIssue = s.BeforPhotoIssue,
BeforPhotoAttachment = s.BeforPhotoAttachment,
AfterPhotoIssue = s.AfterPhotoIssue,
AfterPhotoAttachment = s.AfterPhotoAttachment,
SignOffName = s.SignOffName,
SignOffAttachment = s.SignOffAttachment,
SignOffSignature = s.SignOffSignature,
Comments = s.Comments!.Select(c => new CommentReadModel
{
Id = c.Id,
CreatedDate = c.CreatedDate,
Commenttext = c.Commenttext,
Commenter = c.Commenter,
RecordType = c.RecordType,
CommentType = c.CommentType,
Documents = c.Documents,
UserName = c.ApplicationUser != null ? c.ApplicationUser.FirstName : ""
}).ToList(),
Quotes = s.Quotes!.Select(q => new QuoteRefReadModel
{
Code = q.QuotId,
Id = q.Id
}).ToList(),
AuditLog = _context.WorkOrderAuditLogs
.Where(a => a.WorkOrderId == s.Id)
.OrderByDescending(a => a.CreatedAt)
.Select(a => new AuditLogReadModel
{
Id = a.Id,
FieldName = a.FieldName,
OldValue = a.OldValue,
NewValue = a.NewValue,
Action = a.Action,
CreatedAt = a.CreatedAt,
UserName = a.User != null ? a.User.FirstName + " " + a.User.LastName : ""
}).ToList(),
Dispatches = s.Dispatches!.Select(d => new DispatchReadModel
{
Id = d.Id,
DispatchNumber = d.DispatchNumber,
PONumber = d.PONumber,
NTEAmount = d.NTEAmount,
Description = d.Description,
Status = d.Status,
ScheduledDate = d.ScheduledDate,
CompletedDate = d.CompletedDate,
DispatchedAt = d.DispatchedAt,
EmailSent = d.EmailSent,
VendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
VendorEmail = d.Vendor != null ? d.Vendor.Email : "",
VendorId = d.VendorId
}).ToList()
}).FirstOrDefaultAsync();
}
public async Task<List<WorkOrder>> GetNonTemplateWorkOrdersWithLocationsAsync(int? accountId = null)
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
{
IQueryable<WorkOrder> query = _context.workOrders
.Where(w => w.istemplate != true);
if (accountId.HasValue)
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
return await query
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
.Include(w => w.Locations)
.ToListAsync();
}
private static string? ExtractZip(string? address)
{
if (string.IsNullOrWhiteSpace(address)) return null;
var match = System.Text.RegularExpressions.Regex.Match(address, @"\b(\d{5})(?:-\d{4})?\b");
return match.Success ? match.Groups[1].Value : null;
}
2026-04-28 18:55:14 -05:00
}
}