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 GetVendorByIdAsync(int id) { var vendor = await _vendorDataService.GetByIdWithDetailsAsync(id); return vendor == null ? null : MapToDTO(vendor); } public async Task GetVendorByIdWithDetailsAsync(int id) { var vendor = await _vendorDataService.GetByIdWithDetailsAsync(id); return vendor == null ? null : MapToDTO(vendor); } public async Task> GetAllVendorsAsync() { var vendors = await _vendorDataService.GetAllAsync(); return vendors.Select(MapToDTO); } public async Task> GetVendorsPagedAsync( int page, int pageSize, string? search = null, bool? isActive = true, IReadOnlyCollection? companies = null, IReadOnlyCollection? trades = null, IReadOnlyCollection? locations = null, IReadOnlyCollection? jobBuckets = null) { var (items, totalCount) = await _vendorDataService.GetPagedAsync( page, pageSize, search, isActive, companies, trades, locations, jobBuckets); return new PagedResult { Items = items.Select(MapToDTO).ToList(), TotalCount = totalCount, Page = page, PageSize = pageSize }; } public async Task 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 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 VendorExistsAsync(int id) { return await _vendorDataService.ExistsAsync(id); } public async Task GetTotalVendorCountAsync() { return await _vendorDataService.CountAsync(); } public async Task 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 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(); 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 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(); return dispatch.WorkOrderId.HasValue ? linked.Append(dispatch.WorkOrderId.Value) : linked; }) .Distinct() .Count(); } } }