Files
satellite-provider/SatelliteProvider.Services.RouteManagement/RouteService.cs
T
Oleksandr Bezdieniezhnykh f7ad7aa5ab [AZ-365] Refactor C12: decompose RouteService.CreateRouteAsync
Extract RouteValidator (aggregating validator), RoutePointGraphBuilder
(point interpolation + sequence numbering), GeofenceGridCalculator
(NW/SE region centers), and RouteResponseMapper (entity -> DTO; also
used by GetRouteAsync, eliminating duplicate DTO assembly).

CreateRouteAsync shrinks 184 -> 52 LOC of orchestration. RouteService.cs
shrinks 295 -> 138 LOC overall. Validation aggregates all failures into
a single ArgumentException (AC-2); single-violation messages preserved
verbatim so existing RouteServiceTests pass unchanged. 28 new unit
tests for the four helpers (112/112 unit tests, smoke green).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 02:08:21 +03:00

158 lines
5.3 KiB
C#

using Microsoft.Extensions.Logging;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Interfaces;
using SatelliteProvider.DataAccess.Models;
using SatelliteProvider.DataAccess.Repositories;
namespace SatelliteProvider.Services.RouteManagement;
public class RouteService : IRouteService
{
private readonly IRouteRepository _routeRepository;
private readonly IRegionService _regionService;
private readonly ILogger<RouteService> _logger;
private readonly RouteValidator _validator;
private readonly RoutePointGraphBuilder _pointGraphBuilder;
private readonly GeofenceGridCalculator _geofenceGridCalculator;
private readonly RouteResponseMapper _responseMapper;
public RouteService(
IRouteRepository routeRepository,
IRegionService regionService,
ILogger<RouteService> logger)
: this(routeRepository, regionService, logger,
new RouteValidator(),
new RoutePointGraphBuilder(),
new GeofenceGridCalculator(),
new RouteResponseMapper())
{
}
public RouteService(
IRouteRepository routeRepository,
IRegionService regionService,
ILogger<RouteService> logger,
RouteValidator validator,
RoutePointGraphBuilder pointGraphBuilder,
GeofenceGridCalculator geofenceGridCalculator,
RouteResponseMapper responseMapper)
{
_routeRepository = routeRepository;
_regionService = regionService;
_logger = logger;
_validator = validator;
_pointGraphBuilder = pointGraphBuilder;
_geofenceGridCalculator = geofenceGridCalculator;
_responseMapper = responseMapper;
}
public async Task<RouteResponse> CreateRouteAsync(CreateRouteRequest request)
{
ArgumentNullException.ThrowIfNull(request);
// AZ-362: idempotent POST contract. A retried POST with the same caller-supplied
// Id returns the existing route instead of re-running point generation and
// re-queueing geofence regions.
var existing = await GetRouteAsync(request.Id);
if (existing != null)
{
_logger.LogInformation(
"Idempotent route POST: id {RouteId} already exists; returning existing resource",
request.Id);
return existing;
}
_validator.Validate(request);
var graph = _pointGraphBuilder.Build(request.Points);
var now = DateTime.UtcNow;
var routeEntity = new RouteEntity
{
Id = request.Id,
Name = request.Name,
Description = request.Description,
RegionSizeMeters = request.RegionSizeMeters,
ZoomLevel = request.ZoomLevel,
TotalDistanceMeters = graph.TotalDistanceMeters,
TotalPoints = graph.Points.Count,
RequestMaps = request.RequestMaps,
CreateTilesZip = request.CreateTilesZip,
MapsReady = false,
CreatedAt = now,
UpdatedAt = now,
};
await _routeRepository.InsertRouteAsync(routeEntity);
var pointEntities = graph.Points.Select(p => new RoutePointEntity
{
Id = Guid.NewGuid(),
RouteId = request.Id,
SequenceNumber = p.SequenceNumber,
Latitude = p.Latitude,
Longitude = p.Longitude,
PointType = p.PointType,
SegmentIndex = p.SegmentIndex,
DistanceFromPrevious = p.DistanceFromPrevious,
CreatedAt = now,
}).ToList();
await _routeRepository.InsertRoutePointsAsync(pointEntities);
await ProcessGeofencePolygonsAsync(request);
return _responseMapper.Map(routeEntity, graph.Points);
}
public async Task<RouteResponse?> GetRouteAsync(Guid id)
{
var route = await _routeRepository.GetByIdAsync(id);
if (route == null)
{
return null;
}
var points = await _routeRepository.GetRoutePointsAsync(id);
return _responseMapper.Map(route, points);
}
private async Task ProcessGeofencePolygonsAsync(CreateRouteRequest request)
{
var polygons = request.Geofences?.Polygons;
if (polygons is null || polygons.Count == 0)
{
return;
}
for (int polygonIndex = 0; polygonIndex < polygons.Count; polygonIndex++)
{
var polygon = polygons[polygonIndex];
// Validator (above) guarantees NorthWest/SouthEast are non-null and well-formed.
var regions = _geofenceGridCalculator.GenerateRegions(
polygon.NorthWest!,
polygon.SouthEast!,
request.RegionSizeMeters);
foreach (var center in regions)
{
var geofenceRegionId = Guid.NewGuid();
await _regionService.RequestRegionAsync(
geofenceRegionId,
center.Lat,
center.Lon,
request.RegionSizeMeters,
request.ZoomLevel,
stitchTiles: false);
await _routeRepository.LinkRouteToRegionAsync(
request.Id,
geofenceRegionId,
isGeofence: true,
geofencePolygonIndex: polygonIndex);
}
}
}
}