shoc-backend/SeaHaven.Services/Implementation/VendorService.cs
2026-07-23 19:17:14 -03:00

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();
}
}
}