mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 18:53: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
241 lines
9 KiB
C#
241 lines
9 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>
|
|
/// Location service implementation
|
|
/// Contains business logic, validation, and orchestration for Location operations
|
|
/// </summary>
|
|
public class LocationService : ILocationService
|
|
{
|
|
private readonly ILocationDataService _locationDataService;
|
|
private readonly ICreateLocationValidation _createValidator;
|
|
private readonly IUpdateLocationValidation _updateValidator;
|
|
|
|
public LocationService(
|
|
ILocationDataService locationDataService,
|
|
ICreateLocationValidation createValidator,
|
|
IUpdateLocationValidation updateValidator)
|
|
{
|
|
_locationDataService = locationDataService;
|
|
_createValidator = createValidator;
|
|
_updateValidator = updateValidator;
|
|
}
|
|
|
|
// Query operations
|
|
public async Task<LocationDTO?> GetLocationByIdAsync(int id)
|
|
{
|
|
var location = await _locationDataService.GetByIdAsync(id);
|
|
return location == null ? null : MapToDTO(location);
|
|
}
|
|
|
|
public async Task<LocationDTO?> GetLocationByIdWithDetailsAsync(int id)
|
|
{
|
|
var location = await _locationDataService.GetByIdWithDetailsAsync(id);
|
|
return location == null ? null : MapToDTO(location);
|
|
}
|
|
|
|
public async Task<IEnumerable<LocationDTO>> GetAllLocationsAsync()
|
|
{
|
|
var locations = await _locationDataService.GetAllAsync();
|
|
return locations.Select(MapToDTO);
|
|
}
|
|
|
|
public async Task<IEnumerable<LocationDTO>> GetLocationsByAccountIdAsync(int accountId)
|
|
{
|
|
var locations = await _locationDataService.GetByAccountIdAsync(accountId);
|
|
return locations.Select(MapToDTO);
|
|
}
|
|
|
|
public async Task<PagedResult<LocationDTO>> GetLocationsPagedAsync(int page, int pageSize, string? search = null)
|
|
{
|
|
var (items, totalCount) = await _locationDataService.GetPagedAsync(page, pageSize, search);
|
|
return new PagedResult<LocationDTO>
|
|
{
|
|
Items = items.Select(MapToDTO).ToList(),
|
|
TotalCount = totalCount,
|
|
Page = page,
|
|
PageSize = pageSize
|
|
};
|
|
}
|
|
|
|
public async Task<IReadOnlyList<SiteOptionDTO>> GetSiteOptionsAsync(string? search = null)
|
|
{
|
|
var rows = await _locationDataService.GetSiteOptionsAsync(search);
|
|
return rows
|
|
.Select(r => new SiteOptionDTO
|
|
{
|
|
Id = r.Id,
|
|
Code = r.Code,
|
|
City = r.City,
|
|
State = r.State
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
public async Task<LocationDTO> CreateLocationAsync(CreateLocationDTO dto, string userId)
|
|
{
|
|
var validationResult = await _createValidator.ValidateAsync(dto);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
throw new ValidationException(validationResult.Errors);
|
|
}
|
|
|
|
var location = new Locations
|
|
{
|
|
Name = dto.LocationName,
|
|
Title = dto.LocationName,
|
|
Address1 = dto.Address,
|
|
City = dto.City,
|
|
State = dto.State,
|
|
Zip = dto.Zipcode,
|
|
CreatedDate = DateTime.UtcNow,
|
|
createdby = userId
|
|
};
|
|
|
|
var created = await _locationDataService.AddAsync(location);
|
|
return MapToDTO(created);
|
|
}
|
|
|
|
public async Task<LocationDTO> UpdateLocationAsync(int id, UpdateLocationDTO dto, string userId)
|
|
{
|
|
var validationResult = await _updateValidator.ValidateAsync(dto);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
throw new ValidationException(validationResult.Errors);
|
|
}
|
|
|
|
var location = await _locationDataService.GetByIdAsync(id);
|
|
if (location == null)
|
|
throw new InvalidOperationException($"Location with ID {id} not found");
|
|
|
|
// Update properties
|
|
if (dto.LocationName != null) location.Name = dto.LocationName;
|
|
if (dto.LocationName != null) location.Title = dto.LocationName;
|
|
if (dto.Address != null) location.Address1 = dto.Address;
|
|
if (dto.City != null) location.City = dto.City;
|
|
if (dto.State != null) location.State = dto.State;
|
|
if (dto.Zipcode != null) location.Zip = dto.Zipcode;
|
|
|
|
location.LastModificationTime = DateTime.UtcNow;
|
|
if (int.TryParse(userId, out int userIdInt))
|
|
location.LastModifierUserId = userIdInt;
|
|
|
|
await _locationDataService.UpdateAsync(location);
|
|
return MapToDTO(location);
|
|
}
|
|
|
|
public async Task DeleteLocationAsync(int id, string userId)
|
|
{
|
|
var location = await _locationDataService.GetByIdAsync(id);
|
|
if (location == null)
|
|
throw new InvalidOperationException($"Location with ID {id} not found");
|
|
|
|
location.IsDeleted = true;
|
|
location.DeleterUserId = userId;
|
|
location.DeletionTime = DateTime.UtcNow;
|
|
|
|
await _locationDataService.UpdateAsync(location);
|
|
}
|
|
|
|
public async Task<bool> LocationExistsAsync(int id)
|
|
{
|
|
return await _locationDataService.ExistsAsync(id);
|
|
}
|
|
|
|
public async Task<int> GetTotalLocationCountAsync()
|
|
{
|
|
return await _locationDataService.CountAsync();
|
|
}
|
|
|
|
public async Task<PagedResult<LocationDTO>> GetLocationListPagedAsync(
|
|
int page,
|
|
int pageSize,
|
|
string? search,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var (items, totalCount) = await _locationDataService.GetListPagedAsync(page, pageSize, search, cancellationToken);
|
|
return new PagedResult<LocationDTO>
|
|
{
|
|
Items = items.Select(MapToDTO).ToList(),
|
|
TotalCount = totalCount,
|
|
Page = page,
|
|
PageSize = pageSize
|
|
};
|
|
}
|
|
|
|
public async Task<LocationDTO?> GetLocationDetailAsync(int id, CancellationToken cancellationToken)
|
|
{
|
|
var location = await _locationDataService.GetDetailByIdAsync(id, cancellationToken);
|
|
return location == null ? null : MapToDTO(location);
|
|
}
|
|
|
|
public async Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, CancellationToken cancellationToken)
|
|
{
|
|
var location = new Locations
|
|
{
|
|
Name = request.Name,
|
|
Title = request.Title,
|
|
Address1 = request.Address,
|
|
City = request.City,
|
|
State = request.State,
|
|
Zip = request.ZipCode,
|
|
PhoneNumber = request.Phone,
|
|
Email = request.ContactEmail,
|
|
Status = request.Status
|
|
};
|
|
|
|
await _locationDataService.AddAsync(location, cancellationToken);
|
|
}
|
|
|
|
public async Task UpdateLocationFromRequestAsync(int id, LocationUpdateRequestDTO request, CancellationToken cancellationToken)
|
|
{
|
|
var location = await _locationDataService.GetByIdForUpdateAsync(id, cancellationToken);
|
|
if (location == null)
|
|
throw new KeyNotFoundException($"Location with ID {id} not found");
|
|
|
|
location.Name = request.Name;
|
|
location.Title = request.Title;
|
|
location.Address1 = request.Address;
|
|
location.City = request.City;
|
|
location.State = request.State;
|
|
location.Zip = request.ZipCode;
|
|
location.PhoneNumber = request.Phone;
|
|
location.Email = request.ContactEmail;
|
|
location.Status = request.Status;
|
|
|
|
await _locationDataService.UpdateAsync(location, cancellationToken);
|
|
}
|
|
|
|
public Task<bool> DeleteLocationByIdAsync(int id, CancellationToken cancellationToken)
|
|
{
|
|
return _locationDataService.DeleteByIdAsync(id, cancellationToken);
|
|
}
|
|
|
|
// Private helper methods
|
|
private LocationDTO MapToDTO(Locations location)
|
|
{
|
|
return new LocationDTO
|
|
{
|
|
Id = location.Id,
|
|
Name = location.Name,
|
|
Title = location.Title,
|
|
Address1 = location.Address1,
|
|
Address2 = location.Address2,
|
|
City = location.City,
|
|
State = location.State,
|
|
Zip = location.Zip,
|
|
PhoneNumber = location.PhoneNumber,
|
|
Email = location.Email,
|
|
Status = location.Status,
|
|
CreatedDate = location.CreatedDate,
|
|
CreatedBy = location.createdby
|
|
};
|
|
}
|
|
}
|
|
}
|