mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
* 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
339 lines
13 KiB
C#
339 lines
13 KiB
C#
using Data.SeaHavenIndustries;
|
|
using FluentValidation;
|
|
using SeaHaven.DataServices.Dto;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Interfaces;
|
|
using SeaHaven.Services.Validation;
|
|
|
|
namespace SeaHaven.Services.Implementation
|
|
{
|
|
/// <summary>
|
|
/// Dispatch service implementation
|
|
/// Contains business logic, validation, and orchestration for Dispatch operations
|
|
/// </summary>
|
|
public class DispatchService : IDispatchService
|
|
{
|
|
private readonly IDispatchDataService _dispatchDataService;
|
|
private readonly ICreateDispatchValidation _createValidator;
|
|
private readonly IUpdateDispatchValidation _updateValidator;
|
|
|
|
public DispatchService(
|
|
IDispatchDataService dispatchDataService,
|
|
ICreateDispatchValidation createValidator,
|
|
IUpdateDispatchValidation updateValidator)
|
|
{
|
|
_dispatchDataService = dispatchDataService;
|
|
_createValidator = createValidator;
|
|
_updateValidator = updateValidator;
|
|
}
|
|
|
|
public async Task<DispatchDTO?> GetDispatchByIdAsync(int id)
|
|
{
|
|
var dispatch = await _dispatchDataService.GetByIdAsync(id);
|
|
return dispatch == null ? null : MapToDTO(dispatch);
|
|
}
|
|
|
|
public async Task<DispatchDTO?> GetDispatchByIdWithDetailsAsync(int id)
|
|
{
|
|
var dispatch = await _dispatchDataService.GetByIdWithDetailsAsync(id);
|
|
return dispatch == null ? null : MapToDTO(dispatch);
|
|
}
|
|
|
|
public async Task<IEnumerable<DispatchDTO>> GetAllDispatchesAsync()
|
|
{
|
|
var dispatches = await _dispatchDataService.GetAllAsync();
|
|
return dispatches.Select(MapToDTO);
|
|
}
|
|
|
|
public async Task<IEnumerable<DispatchDTO>> GetDispatchesByVendorIdAsync(int vendorId)
|
|
{
|
|
var dispatches = await _dispatchDataService.GetByVendorIdAsync(vendorId);
|
|
return dispatches.Select(MapToDTO);
|
|
}
|
|
|
|
public async Task<PagedResult<DispatchDTO>> GetDispatchesPagedAsync(int page, int pageSize, string? search = null, string? status = null)
|
|
{
|
|
var (items, totalCount) = await _dispatchDataService.GetPagedAsync(page, pageSize, search, status);
|
|
return new PagedResult<DispatchDTO>
|
|
{
|
|
Items = items.Select(MapToDTO).ToList(),
|
|
TotalCount = totalCount,
|
|
Page = page,
|
|
PageSize = pageSize
|
|
};
|
|
}
|
|
|
|
public async Task<DispatchDTO> CreateDispatchAsync(CreateDispatchDTO dto, string userId)
|
|
{
|
|
var validationResult = await _createValidator.ValidateAsync(dto);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
throw new ValidationException(validationResult.Errors);
|
|
}
|
|
|
|
var dispatchNumber = await GenerateNextDispatchNumberAsync();
|
|
|
|
var dispatch = new Dispatch
|
|
{
|
|
VendorId = dto.VendorId,
|
|
ScheduledDate = dto.ScheduledDate,
|
|
Status = dto.Status ?? "Pending",
|
|
DispatchNumber = dispatchNumber,
|
|
CreatedDate = DateTime.UtcNow,
|
|
createdby = userId
|
|
};
|
|
|
|
var created = await _dispatchDataService.AddAsync(dispatch);
|
|
return MapToDTO(created);
|
|
}
|
|
|
|
public async Task<DispatchDTO> UpdateDispatchAsync(int id, UpdateDispatchDTO dto, string userId)
|
|
{
|
|
var validationResult = await _updateValidator.ValidateAsync(dto);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
throw new ValidationException(validationResult.Errors);
|
|
}
|
|
|
|
var dispatch = await _dispatchDataService.GetByIdAsync(id);
|
|
if (dispatch == null)
|
|
throw new InvalidOperationException($"Dispatch with ID {id} not found");
|
|
|
|
if (dto.VendorId.HasValue) dispatch.VendorId = dto.VendorId.Value;
|
|
if (dto.ScheduledDate.HasValue) dispatch.ScheduledDate = dto.ScheduledDate;
|
|
if (dto.CompletedDate.HasValue) dispatch.CompletedDate = dto.CompletedDate;
|
|
if (dto.Status != null) dispatch.Status = dto.Status;
|
|
|
|
dispatch.LastModificationTime = DateTime.UtcNow;
|
|
if (int.TryParse(userId, out int userIdInt))
|
|
dispatch.LastModifierUserId = userIdInt;
|
|
|
|
await _dispatchDataService.UpdateAsync(dispatch);
|
|
return MapToDTO(dispatch);
|
|
}
|
|
|
|
public async Task DeleteDispatchAsync(int id, string userId)
|
|
{
|
|
var dispatch = await _dispatchDataService.GetByIdAsync(id);
|
|
if (dispatch == null)
|
|
throw new InvalidOperationException($"Dispatch with ID {id} not found");
|
|
|
|
dispatch.IsDeleted = true;
|
|
dispatch.DeleterUserId = userId;
|
|
dispatch.DeletionTime = DateTime.UtcNow;
|
|
|
|
await _dispatchDataService.UpdateAsync(dispatch);
|
|
}
|
|
|
|
public async Task<bool> DispatchExistsAsync(int id)
|
|
{
|
|
return await _dispatchDataService.ExistsAsync(id);
|
|
}
|
|
|
|
public async Task<string> GenerateNextDispatchNumberAsync()
|
|
{
|
|
var count = await _dispatchDataService.CountAsync();
|
|
return $"DISP-{(count + 1):D6}";
|
|
}
|
|
|
|
public async Task<int> GetTotalDispatchCountAsync()
|
|
{
|
|
return await _dispatchDataService.CountAsync();
|
|
}
|
|
|
|
public async Task<DispatchListResultDTO> GetDispatchListAsync(
|
|
string? search,
|
|
string? status,
|
|
int? vendorId,
|
|
DateTime? dateFrom,
|
|
DateTime? dateTo,
|
|
string? sortBy,
|
|
string? sortDir,
|
|
int page,
|
|
int pageSize,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var query = new DispatchListQuery
|
|
{
|
|
Search = search,
|
|
Status = status,
|
|
VendorId = vendorId,
|
|
DateFrom = dateFrom,
|
|
DateTo = dateTo,
|
|
SortBy = sortBy,
|
|
SortDir = sortDir,
|
|
Page = page,
|
|
PageSize = pageSize
|
|
};
|
|
|
|
var result = await _dispatchDataService.GetListAsync(query, cancellationToken);
|
|
|
|
return new DispatchListResultDTO
|
|
{
|
|
Items = result.Items.Select(MapListEntry).ToList(),
|
|
Total = result.Total,
|
|
Page = result.Page,
|
|
PageSize = result.PageSize
|
|
};
|
|
}
|
|
|
|
public async Task<DispatchDetailResultDTO?> GetDispatchDetailAsync(int id, CancellationToken cancellationToken)
|
|
{
|
|
var detail = await _dispatchDataService.GetDetailAsync(id, cancellationToken);
|
|
return detail == null ? null : MapDetail(detail);
|
|
}
|
|
|
|
private static DispatchListEntryDTO MapListEntry(DispatchListItemDTO d)
|
|
{
|
|
return new DispatchListEntryDTO
|
|
{
|
|
Id = d.Id,
|
|
DispatchNumber = d.DispatchNumber,
|
|
PONumber = d.PONumber,
|
|
NTEAmount = d.NTEAmount,
|
|
Status = d.Status,
|
|
DispatchedAt = d.DispatchedAt,
|
|
ScheduledDate = d.ScheduledDate,
|
|
CompletedDate = d.CompletedDate,
|
|
AcknowledgedAt = d.AcknowledgedAt,
|
|
VerifiedAt = d.VerifiedAt,
|
|
VendorId = d.VendorId,
|
|
VendorName = d.VendorName,
|
|
WorkOrderId = d.WorkOrderId,
|
|
PrimaryWOInternalNumber = d.PrimaryWOInternalNumber,
|
|
PrimaryWOTitle = d.PrimaryWOTitle,
|
|
LocationName = d.LocationName,
|
|
WorkOrderCount = d.WorkOrderCount,
|
|
PendingUpliftCount = d.PendingUpliftCount
|
|
};
|
|
}
|
|
|
|
private static DispatchDetailResultDTO MapDetail(DispatchDetailDTO d)
|
|
{
|
|
return new DispatchDetailResultDTO
|
|
{
|
|
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,
|
|
AcknowledgedAt = d.AcknowledgedAt,
|
|
VerifiedAt = d.VerifiedAt,
|
|
VerifiedBy = d.VerifiedBy,
|
|
EmailSent = d.EmailSent,
|
|
WorkOrderId = d.WorkOrderId,
|
|
Vendor = d.Vendor == null ? null : new DispatchDetailVendorResultDTO
|
|
{
|
|
Id = d.Vendor.Id,
|
|
CompanyName = d.Vendor.CompanyName,
|
|
ContactName = d.Vendor.ContactName,
|
|
Email = d.Vendor.Email,
|
|
Phone = d.Vendor.Phone,
|
|
Address = d.Vendor.Address,
|
|
City = d.Vendor.City,
|
|
State = d.Vendor.State,
|
|
Zip = d.Vendor.Zip,
|
|
TradeSpecialties = d.Vendor.TradeSpecialties
|
|
},
|
|
PrimaryLocation = d.PrimaryLocation == null ? null : new DispatchDetailLocationResultDTO
|
|
{
|
|
Id = d.PrimaryLocation.Id,
|
|
Name = d.PrimaryLocation.Name,
|
|
Address1 = d.PrimaryLocation.Address1,
|
|
City = d.PrimaryLocation.City,
|
|
State = d.PrimaryLocation.State,
|
|
Zip = d.PrimaryLocation.Zip
|
|
},
|
|
WorkOrders = d.WorkOrders.Select(w => new DispatchDetailLinkedWorkOrderDTO
|
|
{
|
|
WorkOrderId = w.WorkOrderId,
|
|
InternalWONumber = w.InternalWONumber,
|
|
Title = w.Title,
|
|
Priority = w.Priority,
|
|
Status = w.Status,
|
|
LocationName = w.LocationName,
|
|
Trade = w.Trade,
|
|
SubTrade = w.SubTrade,
|
|
Problem = w.Problem
|
|
}).ToList(),
|
|
Checklist = d.Checklist.Select(c => new DispatchDetailChecklistDTO
|
|
{
|
|
Id = c.Id,
|
|
ItemText = c.ItemText,
|
|
IsCompleted = c.IsCompleted,
|
|
CompletedBy = c.CompletedBy,
|
|
CompletedAt = c.CompletedAt,
|
|
SortOrder = c.SortOrder,
|
|
WorkOrderId = c.WorkOrderId
|
|
}).ToList(),
|
|
Signoffs = d.Signoffs.Select(s => new DispatchDetailSignoffDTO
|
|
{
|
|
Id = s.Id,
|
|
SignoffType = s.SignoffType,
|
|
Name = s.Name,
|
|
Signature = s.Signature,
|
|
SignatureMethod = s.SignatureMethod,
|
|
SignedAt = s.SignedAt
|
|
}).ToList(),
|
|
Comments = d.Comments.Select(c => new DispatchDetailCommentDTO
|
|
{
|
|
Id = c.Id,
|
|
Commenttext = c.Commenttext,
|
|
Commenter = c.Commenter,
|
|
CommentType = c.CommentType,
|
|
RecordType = c.RecordType,
|
|
CreatedDate = c.CreatedDate,
|
|
UserId = c.UserId,
|
|
UserName = c.UserName
|
|
}).ToList(),
|
|
UpliftRequests = d.UpliftRequests.Select(u => new DispatchDetailUpliftDTO
|
|
{
|
|
Id = u.Id,
|
|
CurrentNTE = u.CurrentNTE,
|
|
RequestedNTE = u.RequestedNTE,
|
|
Delta = u.Delta,
|
|
VendorReason = u.VendorReason,
|
|
Status = u.Status,
|
|
RequiredTier = u.RequiredTier,
|
|
RequestedByVendorName = u.RequestedByVendorName,
|
|
RequestedAt = u.RequestedAt,
|
|
DecidedAt = u.DecidedAt,
|
|
DecisionNote = u.DecisionNote,
|
|
DecidedByName = u.DecidedByName
|
|
}).ToList(),
|
|
AuditLog = d.AuditLog.Select(a => new DispatchDetailAuditLogDTO
|
|
{
|
|
Id = a.Id,
|
|
Action = a.Action,
|
|
FieldName = a.FieldName,
|
|
OldValue = a.OldValue,
|
|
NewValue = a.NewValue,
|
|
CreatedAt = a.CreatedAt,
|
|
UserId = a.UserId,
|
|
UserName = a.UserName
|
|
}).ToList()
|
|
};
|
|
}
|
|
|
|
private DispatchDTO MapToDTO(Dispatch dispatch)
|
|
{
|
|
return new DispatchDTO
|
|
{
|
|
Id = dispatch.Id,
|
|
DispatchNumber = dispatch.DispatchNumber,
|
|
Status = dispatch.Status,
|
|
VendorId = dispatch.VendorId,
|
|
VendorName = dispatch.Vendor?.CompanyName,
|
|
ScheduledDate = dispatch.ScheduledDate,
|
|
CompletedDate = dispatch.CompletedDate,
|
|
CreatedDate = dispatch.CreatedDate,
|
|
CreatedBy = dispatch.createdby
|
|
};
|
|
}
|
|
}
|
|
}
|