mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 16:33:12 +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
296 lines
12 KiB
C#
296 lines
12 KiB
C#
using Data.SeaHavenIndustries;
|
|
using FluentValidation;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Interfaces;
|
|
using SeaHaven.Services.Validation;
|
|
|
|
namespace SeaHaven.Services.Implementation
|
|
{
|
|
/// <summary>
|
|
/// Follow-Up service implementation
|
|
/// Contains business logic, validation, and orchestration for Follow-Up operations
|
|
/// </summary>
|
|
public class FollowUpService : IFollowUpService
|
|
{
|
|
private readonly IFollowUpDataService _followUpDataService;
|
|
private readonly ICreateFollowUpValidation _createValidator;
|
|
private readonly IUpdateFollowUpValidation _updateValidator;
|
|
|
|
public FollowUpService(
|
|
IFollowUpDataService followUpDataService,
|
|
ICreateFollowUpValidation createValidator,
|
|
IUpdateFollowUpValidation updateValidator)
|
|
{
|
|
_followUpDataService = followUpDataService;
|
|
_createValidator = createValidator;
|
|
_updateValidator = updateValidator;
|
|
}
|
|
|
|
public async Task<FollowUpDTO?> GetFollowUpByIdAsync(int id)
|
|
{
|
|
var followUp = await _followUpDataService.GetByIdAsync(id);
|
|
return followUp == null ? null : MapToDTO(followUp);
|
|
}
|
|
|
|
public async Task<FollowUpDTO?> GetFollowUpByIdWithDetailsAsync(int id)
|
|
{
|
|
var followUp = await _followUpDataService.GetByIdWithDetailsAsync(id);
|
|
return followUp == null ? null : MapToDTO(followUp);
|
|
}
|
|
|
|
public async Task<IEnumerable<FollowUpDTO>> GetAllFollowUpsAsync()
|
|
{
|
|
var followUps = await _followUpDataService.GetAllAsync();
|
|
return followUps.Select(MapToDTO);
|
|
}
|
|
|
|
public async Task<IEnumerable<FollowUpDTO>> GetFollowUpsByWorkOrderIdAsync(int workOrderId)
|
|
{
|
|
var followUps = await _followUpDataService.GetByWorkOrderIdAsync(workOrderId);
|
|
return followUps.Select(MapToDTO);
|
|
}
|
|
|
|
public async Task<PagedResult<FollowUpDTO>> GetFollowUpsPagedAsync(int page, int pageSize, string? search = null, string? status = null)
|
|
{
|
|
var (items, totalCount) = await _followUpDataService.GetPagedAsync(page, pageSize, search);
|
|
|
|
var filteredItems = items.AsEnumerable();
|
|
if (!string.IsNullOrWhiteSpace(status))
|
|
{
|
|
filteredItems = filteredItems.Where(f => f.Status == status);
|
|
}
|
|
|
|
return new PagedResult<FollowUpDTO>
|
|
{
|
|
Items = filteredItems.Select(MapToDTO).ToList(),
|
|
TotalCount = totalCount,
|
|
Page = page,
|
|
PageSize = pageSize
|
|
};
|
|
}
|
|
|
|
public async Task<FollowUpDTO> CreateFollowUpAsync(CreateFollowUpDTO dto, string userId)
|
|
{
|
|
var validationResult = await _createValidator.ValidateAsync(dto);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
throw new ValidationException(validationResult.Errors);
|
|
}
|
|
|
|
var followUp = new FollowUps
|
|
{
|
|
Reason = dto.Reason,
|
|
ScheduleDate = dto.ScheduleDate,
|
|
ScheduleStartTime = dto.ScheduleStartTime,
|
|
Status = dto.Status,
|
|
EmployeeId = dto.EmployeeId,
|
|
WorkOrderId = dto.WorkOrderId,
|
|
AccountId = dto.AccountId,
|
|
LocationId = dto.LocationId,
|
|
CreatedDate = DateTime.UtcNow,
|
|
createdby = userId
|
|
};
|
|
|
|
var created = await _followUpDataService.AddAsync(followUp);
|
|
return MapToDTO(created);
|
|
}
|
|
|
|
public async Task<FollowUpDTO> UpdateFollowUpAsync(int id, UpdateFollowUpDTO dto, string userId)
|
|
{
|
|
var validationResult = await _updateValidator.ValidateAsync(dto);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
throw new ValidationException(validationResult.Errors);
|
|
}
|
|
|
|
var followUp = await _followUpDataService.GetByIdAsync(id);
|
|
if (followUp == null)
|
|
throw new InvalidOperationException($"Follow-Up with ID {id} not found");
|
|
|
|
if (dto.Reason != null) followUp.Reason = dto.Reason;
|
|
if (dto.ScheduleDate.HasValue) followUp.ScheduleDate = dto.ScheduleDate.Value;
|
|
if (dto.ScheduleStartTime.HasValue) followUp.ScheduleStartTime = dto.ScheduleStartTime.Value;
|
|
if (dto.Status != null) followUp.Status = dto.Status;
|
|
if (dto.EmployeeId.HasValue) followUp.EmployeeId = dto.EmployeeId;
|
|
if (dto.WorkOrderId.HasValue) followUp.WorkOrderId = dto.WorkOrderId;
|
|
if (dto.AccountId.HasValue) followUp.AccountId = dto.AccountId;
|
|
if (dto.LocationId.HasValue) followUp.LocationId = dto.LocationId;
|
|
|
|
followUp.LastModificationTime = DateTime.UtcNow;
|
|
if (int.TryParse(userId, out int userIdInt))
|
|
followUp.LastModifierUserId = userIdInt;
|
|
|
|
await _followUpDataService.UpdateAsync(followUp);
|
|
return MapToDTO(followUp);
|
|
}
|
|
|
|
public async Task DeleteFollowUpAsync(int id, string userId)
|
|
{
|
|
var followUp = await _followUpDataService.GetByIdAsync(id);
|
|
if (followUp == null)
|
|
throw new InvalidOperationException($"Follow-Up with ID {id} not found");
|
|
|
|
followUp.IsDeleted = true;
|
|
followUp.DeleterUserId = userId;
|
|
followUp.DeletionTime = DateTime.UtcNow;
|
|
|
|
await _followUpDataService.UpdateAsync(followUp);
|
|
}
|
|
|
|
public async Task<bool> FollowUpExistsAsync(int id)
|
|
{
|
|
return await _followUpDataService.ExistsAsync(id);
|
|
}
|
|
|
|
public async Task<int> GetTotalFollowUpCountAsync()
|
|
{
|
|
return await _followUpDataService.CountAsync();
|
|
}
|
|
|
|
public async Task<PagedResult<FollowUpListRowDTO>> GetFollowUpListAsync(
|
|
int page,
|
|
int pageSize,
|
|
string? search,
|
|
string? status,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
status = string.IsNullOrWhiteSpace(status) ? "Open" : status;
|
|
|
|
var items = await _followUpDataService.GetListPagedAsync(page, pageSize, search, status, cancellationToken);
|
|
var totalCount = await _followUpDataService.GetListTotalAsync(search, status, cancellationToken);
|
|
|
|
return new PagedResult<FollowUpListRowDTO>
|
|
{
|
|
Items = items.Select(MapToListRow).ToList(),
|
|
TotalCount = totalCount,
|
|
Page = page,
|
|
PageSize = pageSize
|
|
};
|
|
}
|
|
|
|
public async Task<FollowUpDetailDTO?> GetFollowUpDetailAsync(int id, CancellationToken cancellationToken)
|
|
{
|
|
var followUp = await _followUpDataService.GetDetailByIdAsync(id, cancellationToken);
|
|
if (followUp == null)
|
|
return null;
|
|
|
|
var createdByName = await _followUpDataService.GetUserNameAsync(followUp.createdby, cancellationToken);
|
|
|
|
return new FollowUpDetailDTO
|
|
{
|
|
Id = followUp.Id,
|
|
Reason = followUp.Reason,
|
|
ScheduleDate = followUp.ScheduleDate,
|
|
ScheduleStartTime = followUp.ScheduleStartTime,
|
|
EmployeeId = followUp.EmployeeId,
|
|
WorkOrderId = followUp.WorkOrderId,
|
|
AccountId = followUp.AccountId,
|
|
LocationId = followUp.LocationId,
|
|
AccountName = followUp.Account?.Name ?? "",
|
|
AssignedTo = followUp.Employee != null
|
|
? $"{followUp.Employee.FirstName} {followUp.Employee.LastName}"
|
|
: "",
|
|
LocationName = followUp.Location?.Name ?? "",
|
|
WorkOrderNumber = "",
|
|
InternalWONumber = "",
|
|
Status = followUp.Status,
|
|
CreatedDate = followUp.CreatedDate,
|
|
CreatedById = followUp.createdby,
|
|
CreatedByName = createdByName
|
|
};
|
|
}
|
|
|
|
public async Task<int> CreateOpenFollowUpAsync(FollowUpCreateRequestDTO dto, string userId, CancellationToken cancellationToken)
|
|
{
|
|
var followUp = new FollowUps
|
|
{
|
|
Reason = dto.Reason,
|
|
ScheduleDate = dto.ScheduleDate,
|
|
ScheduleStartTime = dto.ScheduleStartTime,
|
|
EmployeeId = dto.EmployeeId,
|
|
WorkOrderId = dto.WorkOrderId,
|
|
AccountId = dto.AccountId,
|
|
LocationId = dto.LocationId,
|
|
Status = "Open",
|
|
createdby = userId
|
|
};
|
|
|
|
var created = await _followUpDataService.AddAsync(followUp, cancellationToken);
|
|
return created.Id;
|
|
}
|
|
|
|
public async Task UpdateFollowUpFieldsAsync(int id, FollowUpUpdateRequestDTO dto, CancellationToken cancellationToken)
|
|
{
|
|
var followUp = await _followUpDataService.GetByIdForUpdateAsync(id, cancellationToken);
|
|
if (followUp == null)
|
|
throw new KeyNotFoundException($"Follow-Up with ID {id} not found");
|
|
|
|
followUp.Reason = dto.Reason;
|
|
followUp.ScheduleDate = dto.ScheduleDate;
|
|
followUp.ScheduleStartTime = dto.ScheduleStartTime;
|
|
followUp.EmployeeId = dto.EmployeeId;
|
|
followUp.WorkOrderId = dto.WorkOrderId;
|
|
followUp.AccountId = dto.AccountId;
|
|
followUp.LocationId = dto.LocationId;
|
|
|
|
await _followUpDataService.UpdateAsync(followUp, cancellationToken);
|
|
}
|
|
|
|
public Task<bool> SetFollowUpStatusAsync(int id, string status, CancellationToken cancellationToken)
|
|
{
|
|
return _followUpDataService.SetStatusAsync(id, status, cancellationToken);
|
|
}
|
|
|
|
public Task<bool> RemoveFollowUpAsync(int id, CancellationToken cancellationToken)
|
|
{
|
|
return _followUpDataService.RemoveByIdAsync(id, cancellationToken);
|
|
}
|
|
|
|
private static FollowUpListRowDTO MapToListRow(FollowUps followUp)
|
|
{
|
|
return new FollowUpListRowDTO
|
|
{
|
|
Id = followUp.Id,
|
|
Reason = followUp.Reason,
|
|
ScheduleDate = followUp.ScheduleDate,
|
|
ScheduleStartTime = followUp.ScheduleStartTime,
|
|
EmployeeId = followUp.EmployeeId,
|
|
WorkOrderId = followUp.WorkOrderId,
|
|
AccountId = followUp.AccountId,
|
|
LocationId = followUp.LocationId,
|
|
AccountName = followUp.Account?.Name ?? "",
|
|
AssignedTo = followUp.Employee != null
|
|
? $"{followUp.Employee.FirstName} {followUp.Employee.LastName}"
|
|
: "",
|
|
LocationName = followUp.Location?.Name ?? "",
|
|
WorkOrderNumber = "",
|
|
InternalWONumber = "",
|
|
Status = followUp.Status,
|
|
CreatedDate = followUp.CreatedDate,
|
|
CreatedById = null,
|
|
CreatedByName = ""
|
|
};
|
|
}
|
|
|
|
private FollowUpDTO MapToDTO(FollowUps followUp)
|
|
{
|
|
return new FollowUpDTO
|
|
{
|
|
Id = followUp.Id,
|
|
Reason = followUp.Reason,
|
|
ScheduleDate = followUp.ScheduleDate,
|
|
ScheduleStartTime = followUp.ScheduleStartTime,
|
|
Status = followUp.Status,
|
|
EmployeeId = followUp.EmployeeId,
|
|
EmployeeName = followUp.Employee != null ? $"{followUp.Employee.FirstName} {followUp.Employee.LastName}" : null,
|
|
WorkOrderId = followUp.WorkOrderId,
|
|
AccountId = followUp.AccountId,
|
|
AccountName = followUp.Account?.Name,
|
|
LocationId = followUp.LocationId,
|
|
LocationName = followUp.Location?.Name,
|
|
CreatedDate = followUp.CreatedDate
|
|
};
|
|
}
|
|
}
|
|
}
|