using FluentValidation; using SeaHaven.Services.DTOs; using SeaHaven.Services.Helpers; namespace SeaHaven.Services.Validation { public interface ICreateVendorValidation : IValidator { } public class CreateVendorValidation : AbstractValidator, ICreateVendorValidation { public CreateVendorValidation() { RuleFor(x => x.Name) .NotEmpty().WithMessage("Vendor name is required") .MaximumLength(200).WithMessage("Vendor name cannot exceed 200 characters"); RuleFor(x => x.Email) .EmailAddress().WithMessage("Invalid email address") .When(x => !string.IsNullOrEmpty(x.Email)); // SH-199: technician (ContactName/Phone) is optional. The company must still // expose at least one contact channel: a company phone or an email. RuleFor(x => x) .Must(dto => !string.IsNullOrWhiteSpace(dto.CompanyPhone) || !string.IsNullOrWhiteSpace(dto.Email)) .WithMessage("At least one company phone or email is required") .OverridePropertyName(nameof(CreateVendorDTO.CompanyPhone)); RuleFor(x => x.ContactName) .MaximumLength(200).WithMessage("Contact name cannot exceed 200 characters") .When(x => !string.IsNullOrEmpty(x.ContactName)); RuleFor(x => x.PreferredContact) .Must(VendorValidationRules.BeValidPreferredContact) .WithMessage("PreferredContact must be one of: Phone, Email, Text") .When(x => !string.IsNullOrEmpty(x.PreferredContact)); RuleFor(x => x.TradeSpecialties) .MaximumLength(1000).WithMessage("Trade specialties cannot exceed 1000 characters") .When(x => !string.IsNullOrEmpty(x.TradeSpecialties)); RuleFor(x => x.Phone) .Must(VendorValidationRules.BeValidNorthAmericanPhone) .WithMessage("Phone must be in North American format: (XXX) XXX-XXXX") .When(x => !string.IsNullOrEmpty(x.Phone)); RuleFor(x => x.CompanyPhone) .Must(VendorValidationRules.BeValidNorthAmericanPhone) .WithMessage("Company phone must be in North American format: (XXX) XXX-XXXX") .When(x => !string.IsNullOrEmpty(x.CompanyPhone)); RuleFor(x => x.Address) .MaximumLength(500).WithMessage("Address cannot exceed 500 characters") .When(x => !string.IsNullOrEmpty(x.Address)); RuleFor(x => x.City) .MaximumLength(100).WithMessage("City cannot exceed 100 characters") .When(x => !string.IsNullOrEmpty(x.City)); RuleFor(x => x.State) .MaximumLength(2).WithMessage("State must be 2 characters") .When(x => !string.IsNullOrEmpty(x.State)); RuleFor(x => x.Zipcode) .MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters") .When(x => !string.IsNullOrEmpty(x.Zipcode)); RuleFor(x => x.GoogleMapsUrl) .Must(VendorValidationRules.BeValidHttpsUrl).WithMessage("Google Maps URL must be a valid HTTPS URL") .When(x => !string.IsNullOrEmpty(x.GoogleMapsUrl)); RuleFor(x => x.Notes) .MaximumLength(2000).WithMessage("Notes cannot exceed 2000 characters") .When(x => !string.IsNullOrEmpty(x.Notes)); } } public interface IUpdateVendorValidation : IValidator { } public class UpdateVendorValidation : AbstractValidator, IUpdateVendorValidation { public UpdateVendorValidation() { RuleFor(x => x.Name) .MaximumLength(200).WithMessage("Vendor name cannot exceed 200 characters") .When(x => !string.IsNullOrEmpty(x.Name)); RuleFor(x => x.Email) .EmailAddress().WithMessage("Invalid email address") .When(x => !string.IsNullOrEmpty(x.Email)); RuleFor(x => x.ContactName) .NotEmpty().WithMessage("Technician name is required") .MaximumLength(200).WithMessage("Contact name cannot exceed 200 characters") .When(x => x.ContactName != null); RuleFor(x => x.PreferredContact) .Must(VendorValidationRules.BeValidPreferredContact) .WithMessage("PreferredContact must be one of: Phone, Email, Text") .When(x => !string.IsNullOrEmpty(x.PreferredContact)); RuleFor(x => x.TradeSpecialties) .MaximumLength(1000).WithMessage("Trade specialties cannot exceed 1000 characters") .When(x => !string.IsNullOrEmpty(x.TradeSpecialties)); RuleFor(x => x.Phone) .Must(VendorValidationRules.BeValidNorthAmericanPhone) .WithMessage("Phone must be in North American format: (XXX) XXX-XXXX") .When(x => !string.IsNullOrEmpty(x.Phone)); RuleFor(x => x.CompanyPhone) .Must(VendorValidationRules.BeValidNorthAmericanPhone) .WithMessage("Company phone must be in North American format: (XXX) XXX-XXXX") .When(x => !string.IsNullOrEmpty(x.CompanyPhone)); RuleFor(x => x.Address) .MaximumLength(500).WithMessage("Address cannot exceed 500 characters") .When(x => !string.IsNullOrEmpty(x.Address)); RuleFor(x => x.City) .MaximumLength(100).WithMessage("City cannot exceed 100 characters") .When(x => !string.IsNullOrEmpty(x.City)); RuleFor(x => x.State) .MaximumLength(2).WithMessage("State must be 2 characters") .When(x => !string.IsNullOrEmpty(x.State)); RuleFor(x => x.Zipcode) .MaximumLength(10).WithMessage("Zipcode cannot exceed 10 characters") .When(x => !string.IsNullOrEmpty(x.Zipcode)); RuleFor(x => x.GoogleMapsUrl) .Must(VendorValidationRules.BeValidHttpsUrl).WithMessage("Google Maps URL must be a valid HTTPS URL") .When(x => !string.IsNullOrEmpty(x.GoogleMapsUrl)); RuleFor(x => x.Notes) .MaximumLength(2000).WithMessage("Notes cannot exceed 2000 characters") .When(x => !string.IsNullOrEmpty(x.Notes)); } } public interface IWorkOrderVendorUpdateValidation : IValidator { } public class WorkOrderVendorUpdateValidation : AbstractValidator, IWorkOrderVendorUpdateValidation { public WorkOrderVendorUpdateValidation() { RuleFor(x => x.WorkOrderId) .GreaterThan(0).WithMessage("A valid work order id is required"); RuleFor(x => x.PreferredContact) .Must(VendorValidationRules.BeValidPreferredContact) .WithMessage("PreferredContact must be one of: Phone, Email, Text") .When(x => !string.IsNullOrEmpty(x.PreferredContact)); RuleFor(x => x.Phone) .Must(VendorValidationRules.BeValidNorthAmericanPhone) .WithMessage("Phone must be in North American format: (XXX) XXX-XXXX") .When(x => !string.IsNullOrEmpty(x.Phone)); RuleFor(x => x.Email) .EmailAddress().WithMessage("Invalid email address") .When(x => !string.IsNullOrEmpty(x.Email)); RuleFor(x => x.Notes) .MaximumLength(2000).WithMessage("Notes cannot exceed 2000 characters") .When(x => !string.IsNullOrEmpty(x.Notes)); RuleFor(x => x.ContactName) .MaximumLength(200).WithMessage("Contact name cannot exceed 200 characters") .When(x => !string.IsNullOrEmpty(x.ContactName)); } } internal static class VendorValidationRules { private static readonly string[] AllowedPreferredContacts = { "Phone", "Email", "Text" }; internal static bool BeValidHttpsUrl(string? value) => Uri.TryCreate(value, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttps; internal static bool BeValidPreferredContact(string? value) => AllowedPreferredContacts.Contains(value, StringComparer.OrdinalIgnoreCase); internal static bool BeValidNorthAmericanPhone(string? value) => value is not null && VendorPhoneNormalizer.NorthAmericanPhoneRegex.IsMatch(value); } }