using FluentValidation; using SatelliteProvider.Common.DTO; namespace SatelliteProvider.Api.Validators; // AZ-809: per-polygon validator invoked via RuleForEach on the parent // CreateRouteRequest (guarded by When(geofences != null) at the parent). // Enforces both corner-point shape and the "NW is north-of and west-of SE" // invariant. // // Error path: errors keys land at `geofences.polygons[i].northWest.lat` etc. public sealed class GeofencePolygonValidator : AbstractValidator { private const double MinLat = -90.0; private const double MaxLat = 90.0; private const double MinLon = -180.0; private const double MaxLon = 180.0; public GeofencePolygonValidator() { // Both corners must be present. Without them no useful range/cross-field // check can run, so short-circuit via .Cascade(CascadeMode.Stop). RuleFor(p => p.NorthWest) .Cascade(CascadeMode.Stop) .NotNull().WithMessage("`northWest` corner is required.") .SetValidator(new GeoCornerValidator("northWest")!); RuleFor(p => p.SouthEast) .Cascade(CascadeMode.Stop) .NotNull().WithMessage("`southEast` corner is required.") .SetValidator(new GeoCornerValidator("southEast")!); // Cross-field invariant: NW must be genuinely north-of (lat greater) // AND west-of (lon smaller) SE. Only runs when both corners survived // the NotNull check above; FluentValidation skips the rule if either // is null (.When(...) guard below). RuleFor(p => p) .Must(HaveNorthWestActuallyNorthOfSouthEast) .When(p => p.NorthWest is not null && p.SouthEast is not null) .WithName("northWest") .WithMessage("`northWest.lat` must be greater than `southEast.lat` (NW is north-of SE)."); RuleFor(p => p) .Must(HaveNorthWestActuallyWestOfSouthEast) .When(p => p.NorthWest is not null && p.SouthEast is not null) .WithName("northWest") .WithMessage("`northWest.lon` must be less than `southEast.lon` (NW is west-of SE)."); } private static bool HaveNorthWestActuallyNorthOfSouthEast(GeofencePolygon polygon) => polygon.NorthWest!.Lat > polygon.SouthEast!.Lat; private static bool HaveNorthWestActuallyWestOfSouthEast(GeofencePolygon polygon) => polygon.NorthWest!.Lon < polygon.SouthEast!.Lon; // Inner per-corner validator. Kept private to this file because the // polygon corners are the only consumer; if a sibling endpoint needs // point-shape validation, promote and rename. private sealed class GeoCornerValidator : AbstractValidator { public GeoCornerValidator(string cornerLabel) { RuleFor(g => g.Lat) .InclusiveBetween(MinLat, MaxLat) .WithMessage($"`{cornerLabel}.lat` must be between {MinLat} and {MaxLat}."); RuleFor(g => g.Lon) .InclusiveBetween(MinLon, MaxLon) .WithMessage($"`{cornerLabel}.lon` must be between {MinLon} and {MaxLon}."); } } }