mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-01 07:33:14 +00:00
523 lines
20 KiB
C#
523 lines
20 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using FluentValidation;
|
|
using FluentValidation.Results;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Helpers;
|
|
using SeaHaven.Services.Interfaces;
|
|
using SeaHaven.Services.Validation;
|
|
|
|
namespace SeaHaven.Services.Implementation
|
|
{
|
|
public class VendorService : IVendorService
|
|
{
|
|
private static readonly string[] TerminalLegacyStatuses =
|
|
{ "completed", "cancelled", "canceled" };
|
|
|
|
private readonly IVendorDataService _vendorDataService;
|
|
private readonly ICreateVendorValidation _createValidator;
|
|
private readonly IUpdateVendorValidation _updateValidator;
|
|
private readonly IWorkOrderVendorUpdateValidation _workOrderUpdateValidator;
|
|
|
|
public VendorService(
|
|
IVendorDataService vendorDataService,
|
|
ICreateVendorValidation createValidator,
|
|
IUpdateVendorValidation updateValidator,
|
|
IWorkOrderVendorUpdateValidation workOrderUpdateValidator)
|
|
{
|
|
_vendorDataService = vendorDataService;
|
|
_createValidator = createValidator;
|
|
_updateValidator = updateValidator;
|
|
_workOrderUpdateValidator = workOrderUpdateValidator;
|
|
}
|
|
|
|
public async Task<VendorDTO?> GetVendorByIdAsync(int id)
|
|
{
|
|
var vendor = await _vendorDataService.GetByIdWithDetailsAsync(id);
|
|
return vendor == null ? null : MapToDTO(vendor);
|
|
}
|
|
|
|
public async Task<VendorDTO?> GetVendorByIdWithDetailsAsync(int id)
|
|
{
|
|
var vendor = await _vendorDataService.GetByIdWithDetailsAsync(id);
|
|
return vendor == null ? null : MapToDTO(vendor);
|
|
}
|
|
|
|
public async Task<IEnumerable<VendorDTO>> GetAllVendorsAsync()
|
|
{
|
|
var vendors = await _vendorDataService.GetAllAsync();
|
|
return vendors.Select(MapToDTO);
|
|
}
|
|
|
|
public async Task<PagedResult<VendorDTO>> GetVendorsPagedAsync(
|
|
int page,
|
|
int pageSize,
|
|
string? search = null,
|
|
bool? isActive = true,
|
|
IReadOnlyCollection<string>? companies = null,
|
|
IReadOnlyCollection<string>? trades = null,
|
|
IReadOnlyCollection<string>? locations = null,
|
|
IReadOnlyCollection<string>? jobBuckets = null)
|
|
{
|
|
var (items, totalCount) = await _vendorDataService.GetPagedAsync(
|
|
page,
|
|
pageSize,
|
|
search,
|
|
isActive,
|
|
companies,
|
|
trades,
|
|
locations,
|
|
jobBuckets);
|
|
return new PagedResult<VendorDTO>
|
|
{
|
|
Items = items.Select(MapToDTO).ToList(),
|
|
TotalCount = totalCount,
|
|
Page = page,
|
|
PageSize = pageSize
|
|
};
|
|
}
|
|
|
|
public async Task<VendorDTO> CreateVendorAsync(CreateVendorDTO dto, string userId)
|
|
{
|
|
NormalizePhoneFields(dto);
|
|
var validationResult = await _createValidator.ValidateAsync(dto);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
throw new ValidationException(validationResult.Errors);
|
|
}
|
|
|
|
var company = await ResolveCompanyAsync(dto.CompanyId, dto.Name, dto, userId);
|
|
|
|
var vendor = new Vendor
|
|
{
|
|
CompanyName = dto.Name,
|
|
ContactName = dto.ContactName,
|
|
Email = dto.Email,
|
|
Phone = VendorPhoneNormalizer.NormalizeToCanonical(dto.Phone),
|
|
CompanyPhone = VendorPhoneNormalizer.NormalizeToCanonical(dto.CompanyPhone),
|
|
PreferredContact = dto.PreferredContact,
|
|
Address = dto.Address,
|
|
City = dto.City,
|
|
State = dto.State,
|
|
Zip = dto.Zipcode,
|
|
TradeSpecialties = dto.TradeSpecialties,
|
|
GoogleMapsUrl = dto.GoogleMapsUrl,
|
|
Notes = dto.Notes,
|
|
IsActive = dto.IsActive,
|
|
CompanyId = company?.Id,
|
|
CreatedDate = DateTime.UtcNow,
|
|
createdby = userId
|
|
};
|
|
|
|
ApplyCompanyFields(vendor, company);
|
|
|
|
var created = await _vendorDataService.AddAsync(vendor);
|
|
return MapToDTO(created);
|
|
}
|
|
|
|
public async Task<VendorDTO> UpdateVendorAsync(int id, UpdateVendorDTO dto, string userId)
|
|
{
|
|
NormalizePhoneFields(dto);
|
|
var validationResult = await _updateValidator.ValidateAsync(dto);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
throw new ValidationException(validationResult.Errors);
|
|
}
|
|
|
|
var vendor = await _vendorDataService.GetByIdAsync(id);
|
|
if (vendor == null)
|
|
throw new InvalidOperationException($"Vendor with ID {id} not found");
|
|
|
|
if (dto.IsActive.HasValue && !dto.IsActive.Value)
|
|
await AssertNoOpenLinkedWorkOrdersAsync(id);
|
|
|
|
if (dto.Name != null) vendor.CompanyName = dto.Name;
|
|
if (dto.ContactName != null) vendor.ContactName = dto.ContactName;
|
|
if (dto.Email != null) vendor.Email = dto.Email;
|
|
if (dto.Phone != null) vendor.Phone = VendorPhoneNormalizer.NormalizeToCanonical(dto.Phone);
|
|
if (dto.PreferredContact != null) vendor.PreferredContact = dto.PreferredContact;
|
|
if (dto.TradeSpecialties != null) vendor.TradeSpecialties = dto.TradeSpecialties;
|
|
if (dto.Notes != null) vendor.Notes = dto.Notes;
|
|
if (dto.IsActive.HasValue) vendor.IsActive = dto.IsActive.Value;
|
|
|
|
if (dto.CompanyId.HasValue || dto.Name != null || HasCompanyFieldUpdate(dto))
|
|
{
|
|
var company = await ResolveCompanyAsync(
|
|
dto.CompanyId ?? vendor.CompanyId,
|
|
dto.Name ?? vendor.CompanyName,
|
|
dto,
|
|
userId);
|
|
vendor.CompanyId = company?.Id;
|
|
ApplyCompanyFields(vendor, company);
|
|
}
|
|
|
|
vendor.LastModificationTime = DateTime.UtcNow;
|
|
if (int.TryParse(userId, out int userIdInt))
|
|
vendor.LastModifierUserId = userIdInt;
|
|
|
|
await _vendorDataService.UpdateAsync(vendor);
|
|
return MapToDTO(vendor);
|
|
}
|
|
|
|
public async Task DeleteVendorAsync(int id, string userId)
|
|
{
|
|
var vendor = await _vendorDataService.GetByIdAsync(id);
|
|
if (vendor == null)
|
|
throw new InvalidOperationException($"Vendor with ID {id} not found");
|
|
|
|
await AssertNoOpenLinkedWorkOrdersAsync(id);
|
|
|
|
vendor.IsActive = false;
|
|
vendor.LastModificationTime = DateTime.UtcNow;
|
|
if (int.TryParse(userId, out int userIdInt))
|
|
vendor.LastModifierUserId = userIdInt;
|
|
|
|
await _vendorDataService.UpdateAsync(vendor);
|
|
}
|
|
|
|
public async Task<bool> VendorExistsAsync(int id)
|
|
{
|
|
return await _vendorDataService.ExistsAsync(id);
|
|
}
|
|
|
|
public async Task<int> GetTotalVendorCountAsync()
|
|
{
|
|
return await _vendorDataService.CountAsync();
|
|
}
|
|
|
|
public async Task<VendorDeactivationImpactDTO> GetDeactivationImpactAsync(int vendorId)
|
|
{
|
|
if (!await _vendorDataService.ExistsAsync(vendorId))
|
|
throw new InvalidOperationException($"Vendor with ID {vendorId} not found");
|
|
|
|
var linked = await _vendorDataService.GetLinkedWorkOrdersAsync(vendorId);
|
|
var openWorkOrders = linked
|
|
.Where(wo => !IsTerminalWorkOrderStatus(wo.LifecycleStatus, wo.Status))
|
|
.Select(MapToLinkedWorkOrderDTO)
|
|
.ToList();
|
|
|
|
return new VendorDeactivationImpactDTO
|
|
{
|
|
VendorId = vendorId,
|
|
CanDeactivate = openWorkOrders.Count == 0,
|
|
OpenWorkOrders = openWorkOrders
|
|
};
|
|
}
|
|
|
|
public async Task<VendorDTO> UpdateVendorFromWorkOrderAsync(
|
|
int vendorId,
|
|
WorkOrderVendorUpdateDTO dto,
|
|
string userId)
|
|
{
|
|
dto.Phone = VendorPhoneNormalizer.NormalizeToCanonical(dto.Phone);
|
|
var validationResult = await _workOrderUpdateValidator.ValidateAsync(dto);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
throw new ValidationException(validationResult.Errors);
|
|
}
|
|
|
|
var vendor = await _vendorDataService.GetByIdAsync(vendorId);
|
|
if (vendor == null)
|
|
throw new InvalidOperationException($"Vendor with ID {vendorId} not found");
|
|
|
|
var isAssigned = await _vendorDataService.IsVendorAssignedToWorkOrderAsync(vendorId, dto.WorkOrderId);
|
|
if (!isAssigned)
|
|
throw new InvalidOperationException(
|
|
$"Vendor {vendorId} is not actively linked to work order {dto.WorkOrderId}.");
|
|
|
|
var changes = new List<VendorAuditLog>();
|
|
var now = DateTime.UtcNow;
|
|
|
|
void RecordChange(string fieldName, string? oldValue, string? newValue)
|
|
{
|
|
if (!string.Equals(oldValue, newValue, StringComparison.Ordinal))
|
|
{
|
|
changes.Add(new VendorAuditLog
|
|
{
|
|
VendorId = vendorId,
|
|
WorkOrderId = dto.WorkOrderId,
|
|
FieldName = fieldName,
|
|
OldValue = oldValue,
|
|
NewValue = newValue,
|
|
Actor = userId,
|
|
CreatedAt = now
|
|
});
|
|
}
|
|
}
|
|
|
|
if (dto.ContactName != null)
|
|
{
|
|
RecordChange(nameof(Vendor.ContactName), vendor.ContactName, dto.ContactName);
|
|
vendor.ContactName = dto.ContactName;
|
|
}
|
|
|
|
if (dto.PreferredContact != null)
|
|
{
|
|
RecordChange(nameof(Vendor.PreferredContact), vendor.PreferredContact, dto.PreferredContact);
|
|
vendor.PreferredContact = dto.PreferredContact;
|
|
}
|
|
|
|
if (dto.Phone != null)
|
|
{
|
|
var normalized = VendorPhoneNormalizer.NormalizeToCanonical(dto.Phone);
|
|
RecordChange(nameof(Vendor.Phone), vendor.Phone, normalized);
|
|
vendor.Phone = normalized;
|
|
}
|
|
|
|
if (dto.Email != null)
|
|
{
|
|
RecordChange(nameof(Vendor.Email), vendor.Email, dto.Email);
|
|
vendor.Email = dto.Email;
|
|
}
|
|
|
|
if (dto.Notes != null)
|
|
{
|
|
RecordChange(nameof(Vendor.Notes), vendor.Notes, dto.Notes);
|
|
vendor.Notes = dto.Notes;
|
|
}
|
|
|
|
vendor.LastModificationTime = now;
|
|
if (int.TryParse(userId, out int userIdInt))
|
|
vendor.LastModifierUserId = userIdInt;
|
|
|
|
await _vendorDataService.UpdateWithAuditLogsAsync(vendor, changes);
|
|
|
|
return MapToDTO(vendor);
|
|
}
|
|
|
|
private async Task<VendorCompany?> ResolveCompanyAsync(
|
|
int? requestedCompanyId,
|
|
string? companyName,
|
|
object dto,
|
|
string userId)
|
|
{
|
|
if (requestedCompanyId.HasValue)
|
|
{
|
|
var byId = await _vendorDataService.GetCompanyByIdAsync(requestedCompanyId.Value);
|
|
if (byId != null)
|
|
{
|
|
ApplyCompanyUpdates(byId, companyName, dto, userId);
|
|
await _vendorDataService.UpdateCompanyAsync(byId);
|
|
return byId;
|
|
}
|
|
|
|
throw new ValidationException(new[]
|
|
{
|
|
new ValidationFailure(
|
|
nameof(CreateVendorDTO.CompanyId),
|
|
$"No vendor company exists with ID {requestedCompanyId.Value}.")
|
|
});
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(companyName))
|
|
return null;
|
|
|
|
var normalizedName = companyName.Trim().ToLowerInvariant();
|
|
var existing = await _vendorDataService.GetCompanyByNormalizedNameAsync(normalizedName);
|
|
if (existing != null)
|
|
{
|
|
ApplyCompanyUpdates(existing, companyName, dto, userId);
|
|
await _vendorDataService.UpdateCompanyAsync(existing);
|
|
return existing;
|
|
}
|
|
|
|
string? companyPhone = null;
|
|
string? email = null;
|
|
string? address = null;
|
|
string? city = null;
|
|
string? state = null;
|
|
string? zip = null;
|
|
string? mapsUrl = null;
|
|
|
|
if (dto is CreateVendorDTO create)
|
|
{
|
|
companyPhone = VendorPhoneNormalizer.NormalizeToCanonical(create.CompanyPhone);
|
|
email = create.Email;
|
|
address = create.Address;
|
|
city = create.City;
|
|
state = create.State;
|
|
zip = create.Zipcode;
|
|
mapsUrl = create.GoogleMapsUrl;
|
|
}
|
|
else if (dto is UpdateVendorDTO update)
|
|
{
|
|
companyPhone = VendorPhoneNormalizer.NormalizeToCanonical(update.CompanyPhone);
|
|
email = update.Email;
|
|
address = update.Address;
|
|
city = update.City;
|
|
state = update.State;
|
|
zip = update.Zipcode;
|
|
mapsUrl = update.GoogleMapsUrl;
|
|
}
|
|
|
|
var company = new VendorCompany
|
|
{
|
|
Name = companyName.Trim(),
|
|
NormalizedName = normalizedName,
|
|
CompanyPhone = companyPhone,
|
|
Email = email,
|
|
Address = address,
|
|
City = city,
|
|
State = state,
|
|
Zip = zip,
|
|
GoogleMapsUrl = mapsUrl,
|
|
CreatedDate = DateTime.UtcNow,
|
|
createdby = userId
|
|
};
|
|
|
|
return await _vendorDataService.AddCompanyAsync(company);
|
|
}
|
|
|
|
private static void ApplyCompanyFields(Vendor vendor, VendorCompany? company)
|
|
{
|
|
if (company == null)
|
|
return;
|
|
|
|
if (company.CompanyPhone != null) vendor.CompanyPhone = company.CompanyPhone;
|
|
if (company.Address != null) vendor.Address = company.Address;
|
|
if (company.City != null) vendor.City = company.City;
|
|
if (company.State != null) vendor.State = company.State;
|
|
if (company.Zip != null) vendor.Zip = company.Zip;
|
|
if (company.GoogleMapsUrl != null) vendor.GoogleMapsUrl = company.GoogleMapsUrl;
|
|
}
|
|
|
|
private static bool HasCompanyFieldUpdate(UpdateVendorDTO dto) =>
|
|
dto.CompanyPhone != null ||
|
|
dto.Address != null ||
|
|
dto.City != null ||
|
|
dto.State != null ||
|
|
dto.Zipcode != null ||
|
|
dto.GoogleMapsUrl != null;
|
|
|
|
private static void ApplyCompanyUpdates(
|
|
VendorCompany company,
|
|
string? companyName,
|
|
object dto,
|
|
string userId)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(companyName))
|
|
{
|
|
company.Name = companyName.Trim();
|
|
company.NormalizedName = company.Name.ToLowerInvariant();
|
|
}
|
|
|
|
if (dto is CreateVendorDTO create)
|
|
{
|
|
if (create.CompanyPhone != null) company.CompanyPhone = create.CompanyPhone;
|
|
if (create.Address != null) company.Address = create.Address;
|
|
if (create.City != null) company.City = create.City;
|
|
if (create.State != null) company.State = create.State;
|
|
if (create.Zipcode != null) company.Zip = create.Zipcode;
|
|
if (create.GoogleMapsUrl != null) company.GoogleMapsUrl = create.GoogleMapsUrl;
|
|
}
|
|
else if (dto is UpdateVendorDTO update)
|
|
{
|
|
if (update.CompanyPhone != null) company.CompanyPhone = update.CompanyPhone;
|
|
if (update.Address != null) company.Address = update.Address;
|
|
if (update.City != null) company.City = update.City;
|
|
if (update.State != null) company.State = update.State;
|
|
if (update.Zipcode != null) company.Zip = update.Zipcode;
|
|
if (update.GoogleMapsUrl != null) company.GoogleMapsUrl = update.GoogleMapsUrl;
|
|
}
|
|
|
|
company.LastModificationTime = DateTime.UtcNow;
|
|
if (int.TryParse(userId, out int userIdInt))
|
|
company.LastModifierUserId = userIdInt;
|
|
}
|
|
|
|
private static void NormalizePhoneFields(CreateVendorDTO dto)
|
|
{
|
|
dto.Phone = VendorPhoneNormalizer.NormalizeToCanonical(dto.Phone);
|
|
dto.CompanyPhone = VendorPhoneNormalizer.NormalizeToCanonical(dto.CompanyPhone);
|
|
}
|
|
|
|
private static void NormalizePhoneFields(UpdateVendorDTO dto)
|
|
{
|
|
dto.Phone = VendorPhoneNormalizer.NormalizeToCanonical(dto.Phone);
|
|
dto.CompanyPhone = VendorPhoneNormalizer.NormalizeToCanonical(dto.CompanyPhone);
|
|
}
|
|
|
|
private async Task AssertNoOpenLinkedWorkOrdersAsync(int vendorId)
|
|
{
|
|
var linked = await _vendorDataService.GetLinkedWorkOrdersAsync(vendorId);
|
|
var openWorkOrders = linked
|
|
.Where(wo => !IsTerminalWorkOrderStatus(wo.LifecycleStatus, wo.Status))
|
|
.Select(MapToLinkedWorkOrderDTO)
|
|
.ToList();
|
|
|
|
if (openWorkOrders.Count > 0)
|
|
{
|
|
throw new VendorDeactivationBlockedException(
|
|
"Vendor cannot be deactivated while linked work orders are open or scheduled.",
|
|
openWorkOrders);
|
|
}
|
|
}
|
|
|
|
private static bool IsTerminalWorkOrderStatus(LifecycleStatus? lifecycleStatus, string? legacyStatus)
|
|
{
|
|
if (lifecycleStatus == LifecycleStatus.Completed || lifecycleStatus == LifecycleStatus.Canceled)
|
|
return true;
|
|
|
|
if (!string.IsNullOrWhiteSpace(legacyStatus) &&
|
|
TerminalLegacyStatuses.Contains(legacyStatus.Trim().ToLowerInvariant()))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
private static LinkedWorkOrderDTO MapToLinkedWorkOrderDTO(LinkedWorkOrderInfo info) => new()
|
|
{
|
|
WorkOrderId = info.WorkOrderId,
|
|
WorkOrderNumber = info.WorkOrderNumber,
|
|
WorkOrderTitle = info.WorkOrderTitle,
|
|
Status = info.Status,
|
|
ScheduledDate = info.ScheduledDate,
|
|
DispatchId = info.DispatchId
|
|
};
|
|
|
|
private VendorDTO MapToDTO(Vendor vendor)
|
|
{
|
|
return new VendorDTO
|
|
{
|
|
Id = vendor.Id,
|
|
Name = vendor.CompanyName,
|
|
CompanyId = vendor.CompanyId,
|
|
ContactName = vendor.ContactName,
|
|
Email = vendor.Email,
|
|
Phone = vendor.Phone,
|
|
CompanyPhone = vendor.CompanyPhone,
|
|
PreferredContact = vendor.PreferredContact,
|
|
Address = vendor.Address,
|
|
City = vendor.City,
|
|
State = vendor.State,
|
|
Zipcode = vendor.Zip,
|
|
TradeSpecialties = vendor.TradeSpecialties,
|
|
GoogleMapsUrl = vendor.GoogleMapsUrl,
|
|
Notes = vendor.Notes,
|
|
IsActive = vendor.IsActive,
|
|
TotalJobs = vendor.Dispatches == null
|
|
? vendor.TotalJobs
|
|
: CountDistinctWorkOrders(vendor)
|
|
};
|
|
}
|
|
|
|
private static int CountDistinctWorkOrders(Vendor vendor)
|
|
{
|
|
if (vendor.Dispatches == null)
|
|
return 0;
|
|
|
|
return vendor.Dispatches
|
|
.SelectMany(dispatch =>
|
|
{
|
|
var linked = dispatch.DispatchWorkOrders?.Select(item => item.WorkOrderId)
|
|
?? Enumerable.Empty<int>();
|
|
return dispatch.WorkOrderId.HasValue
|
|
? linked.Append(dispatch.WorkOrderId.Value)
|
|
: linked;
|
|
})
|
|
.Distinct()
|
|
.Count();
|
|
}
|
|
}
|
|
}
|