using FluentValidation; public class LoginRequestValidator : AbstractValidator { public LoginRequestValidator() { RuleFor(x => x.Username).NotEmpty(); RuleFor(x => x.Password).NotEmpty(); } } public class RefreshRequestValidator : AbstractValidator { public RefreshRequestValidator() { RuleFor(x => x.RefreshToken).NotEmpty(); } } public class LogoutRequestValidator : AbstractValidator { public LogoutRequestValidator() { RuleFor(x => x.RefreshToken).NotEmpty(); } } public class UpsertDraftPatientRequestValidator : AbstractValidator { public UpsertDraftPatientRequestValidator() { RuleFor(x => x.FullName).MaximumLength(200).When(x => x.FullName is not null); RuleFor(x => x.DateOfBirth).LessThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow)) .WithMessage("Date of birth cannot be in the future.") .When(x => x.DateOfBirth.HasValue); RuleFor(x => x.Sex) .Must(s => IsAllowedSex(s)) .WithMessage("Sex must be 'male', 'female', 'other', or 'unknown'.") .When(x => x.Sex is not null); } private static bool IsAllowedSex(string? sex) { if (sex is null) return true; return sex.Trim().ToLowerInvariant() switch { "m" or "male" or "f" or "female" or "other" or "unknown" => true, _ => false }; } } public class UpsertDraftEncounterRequestValidator : AbstractValidator { public UpsertDraftEncounterRequestValidator() { RuleFor(x => x.AdmissionDate).LessThanOrEqualTo(DateTimeOffset.UtcNow) .WithMessage("Admission date cannot be in the future.") .When(x => x.AdmissionDate.HasValue); RuleFor(x => x.RoomBed).MaximumLength(50).When(x => x.RoomBed is not null); RuleFor(x => x.AdmissionReason).MaximumLength(500).When(x => x.AdmissionReason is not null); RuleFor(x => x.DischargeDiagnosis).MaximumLength(500).When(x => x.DischargeDiagnosis is not null); } } public class CreateDraftObservationRequestValidator : AbstractValidator { public CreateDraftObservationRequestValidator() { RuleFor(x => x.ObservationCode).NotEmpty(); RuleFor(x => x.Unit).NotEmpty(); RuleFor(x => x.RecordedAt).NotEmpty() .LessThanOrEqualTo(DateTimeOffset.UtcNow.AddMinutes(5)) .WithMessage("RecordedAt cannot be in the future."); } } public class UpdateDraftObservationRequestValidator : AbstractValidator { public UpdateDraftObservationRequestValidator() { RuleFor(x => x.ObservationCode).NotEmpty(); RuleFor(x => x.Unit).NotEmpty(); RuleFor(x => x.RecordedAt).NotEmpty() .LessThanOrEqualTo(DateTimeOffset.UtcNow.AddMinutes(5)) .WithMessage("RecordedAt cannot be in the future."); } } public class VerifyBatchRequestValidator : AbstractValidator { private static readonly HashSet _validStatuses = new() { "ok", "warning", "error" }; public VerifyBatchRequestValidator() { RuleFor(x => x.FieldChecks).NotEmpty() .WithMessage("At least one field check is required."); RuleForEach(x => x.FieldChecks).ChildRules(check => { check.RuleFor(c => c.FieldName).NotEmpty(); check.RuleFor(c => c.Status).NotEmpty() .Must(s => _validStatuses.Contains(s)) .WithMessage("Status must be 'ok', 'warning', or 'error'."); }); } } public class RejectBatchRequestValidator : AbstractValidator { public RejectBatchRequestValidator() { RuleFor(x => x.Reason) .NotEmpty() .WithErrorCode("REJECTION_REASON_REQUIRED") .WithMessage("Rejection reason is required."); RuleFor(x => x.Reason) .MinimumLength(10) .When(x => !string.IsNullOrEmpty(x.Reason)) .WithErrorCode("REJECTION_REASON_TOO_SHORT") .WithMessage("Rejection reason must be at least 10 characters."); } } public class RecordObservationsRequestValidator : AbstractValidator { public RecordObservationsRequestValidator() { RuleFor(x => x.Observations) .NotEmpty() .WithErrorCode("EMPTY_OBSERVATIONS") .WithMessage("At least one observation is required.") .Must(o => o.Count <= 10) .WithMessage("Maximum 10 observations per request."); RuleFor(x => x.ClinicianAttestation) .Equal(true) .WithErrorCode("ATTESTATION_REQUIRED") .WithMessage("Clinician attestation is required."); RuleFor(x => x.PasswordConfirm).NotEmpty(); RuleForEach(x => x.Observations).SetValidator(new LiveCaptureObservationRequestValidator()); } } public class OpenEncounterWithVitalsRequestValidator : AbstractValidator { public OpenEncounterWithVitalsRequestValidator() { RuleFor(x => x.PatientId).NotEmpty(); RuleFor(x => x.Department).NotEmpty(); RuleFor(x => x.AdmissionReason).NotEmpty(); RuleFor(x => x.Observations) .NotEmpty() .WithErrorCode("EMPTY_OBSERVATIONS") .WithMessage("At least one observation is required.") .Must(o => o.Count <= 10) .WithMessage("Maximum 10 observations per request."); RuleFor(x => x.ClinicianAttestation) .Equal(true) .WithErrorCode("ATTESTATION_REQUIRED") .WithMessage("Clinician attestation is required."); RuleFor(x => x.PasswordConfirm).NotEmpty(); RuleForEach(x => x.Observations).SetValidator(new LiveCaptureObservationRequestValidator()); } } public class LiveCaptureObservationRequestValidator : AbstractValidator { public LiveCaptureObservationRequestValidator() { RuleFor(x => x.ObservationCode).NotEmpty(); RuleFor(x => x.Unit).NotEmpty(); RuleFor(x => x.RecordedAt).NotEmpty(); } } public class CreateUserRequestValidator : AbstractValidator { private static readonly HashSet _validRoles = new() { "INTAKE_CLERK", "DATA_ENTRY_CLERK", "VERIFIER", "CLINICAL_APPROVER", "CLINICIAN", "ADMINISTRATOR" }; public CreateUserRequestValidator() { RuleFor(x => x.Username).NotEmpty().MaximumLength(50); RuleFor(x => x.Password).NotEmpty().MinimumLength(8) .Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter.") .Matches("[0-9]").WithMessage("Password must contain at least one digit."); RuleFor(x => x.FullName).NotEmpty().MaximumLength(200); RuleFor(x => x.Role).NotEmpty() .Must(r => _validRoles.Contains(r)) .WithMessage("Role must be one of: INTAKE_CLERK, DATA_ENTRY_CLERK, VERIFIER, CLINICAL_APPROVER, CLINICIAN, ADMINISTRATOR."); } } public class UpdateUserRequestValidator : AbstractValidator { private static readonly HashSet _validRoles = new() { "INTAKE_CLERK", "DATA_ENTRY_CLERK", "VERIFIER", "CLINICAL_APPROVER", "CLINICIAN", "ADMINISTRATOR" }; public UpdateUserRequestValidator() { RuleFor(x => x.FullName).MaximumLength(200).When(x => x.FullName is not null); RuleFor(x => x.Role) .Must(r => _validRoles.Contains(r!)) .WithMessage("Role must be one of: INTAKE_CLERK, DATA_ENTRY_CLERK, VERIFIER, CLINICAL_APPROVER, CLINICIAN, ADMINISTRATOR.") .When(x => x.Role is not null); } } public class ResetPasswordRequestValidator : AbstractValidator { public ResetPasswordRequestValidator() { RuleFor(x => x.NewPassword).NotEmpty().MinimumLength(8) .Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter.") .Matches("[0-9]").WithMessage("Password must contain at least one digit."); } } public class ChangePasswordRequestValidator : AbstractValidator { public ChangePasswordRequestValidator() { RuleFor(x => x.CurrentPassword).NotEmpty(); RuleFor(x => x.NewPassword).NotEmpty().MinimumLength(8) .Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter.") .Matches("[0-9]").WithMessage("Password must contain at least one digit."); } } public class CancelBatchRequestValidator : AbstractValidator { public CancelBatchRequestValidator() { RuleFor(x => x.Reason).NotEmpty().MinimumLength(5) .WithMessage("Cancellation reason must be at least 5 characters."); } }