From 23ab05766d8203d1e569b9f9c9547a97f9dcc5e6 Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Mon, 11 May 2026 03:55:22 +0300 Subject: [PATCH] [AZ-370] Refactor C17: status / point-type enums + AC RT2 update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces bare strings with two enums in Common/Enums/: RegionStatus { Queued, Processing, Completed, Failed } RoutePointType { Start, End, Action, Intermediate } Adds a Dapper EnumStringTypeHandler (DataAccess/TypeHandlers/) that round-trips enums to/from lowercase strings, registered once at startup via DapperEnumTypeHandlers.RegisterAll(). DataAccess now references Common (project ref) so entities can carry the enum types. Sites converted: RegionService (5), RouteProcessingService (3), RoutePointGraphBuilder (4), entity Status/PointType columns. Log message and summary file format preserved via .ToLowerInvariant(). API JSON contract preserved by adding JsonStringEnumConverter with JsonNamingPolicy.CamelCase to the http JSON options — single-word enum members serialize to the same lowercase strings as before. DTO renamed: Common.DTO.RegionStatus -> RegionStatusResponse to free the RegionStatus name for the new enum (forced by the task's explicit enum name); the renamed DTO has no public-API impact at the JSON wire level. Stale doc references updated. AC RT2 in _docs/00_problem/acceptance_criteria.md now lists all 4 point types (start/end/action/intermediate). Tests: 171 / 171 unit + 5 / 5 smoke green (was 141 + 5; +30 new tests covering type handler round-trip, set/parse, unknown-value rejection, idempotent registration, and the AC RT2 doc check). Co-authored-by: Cursor --- SatelliteProvider.Api/Program.cs | 5 + ...egionStatus.cs => RegionStatusResponse.cs} | 7 +- SatelliteProvider.Common/DTO/RoutePointDto.cs | 5 +- .../Enums/RegionStatus.cs | 9 ++ .../Enums/RoutePointType.cs | 9 ++ .../Interfaces/IRegionService.cs | 4 +- .../Models/RegionEntity.cs | 4 +- .../Models/RoutePointEntity.cs | 4 +- .../Repositories/IRegionRepository.cs | 3 +- .../Repositories/RegionRepository.cs | 3 +- .../SatelliteProvider.DataAccess.csproj | 4 + .../TypeHandlers/EnumStringTypeHandler.cs | 51 +++++++ .../RegionService.cs | 23 +-- .../RoutePointGraphBuilder.cs | 5 +- .../RouteProcessingService.cs | 7 +- .../AcceptanceCriteriaRT2Tests.cs | 40 +++++ .../EnumStringTypeHandlerTests.cs | 140 ++++++++++++++++++ SatelliteProvider.Tests/RegionServiceTests.cs | 33 +++-- .../RoutePointGraphBuilderTests.cs | 15 +- .../RouteResponseMapperTests.cs | 13 +- SatelliteProvider.Tests/RouteServiceTests.cs | 35 ++--- _docs/00_problem/acceptance_criteria.md | 2 +- _docs/02_document/00_discovery.md | 2 +- _docs/02_document/04_verification_log.md | 2 +- .../components/01_common/description.md | 4 +- .../02_document/modules/common_interfaces.md | 4 +- _docs/02_document/modules/services_region.md | 6 +- .../AZ-370_refactor_status_pointtype_enums.md | 0 _docs/_autodev_state.md | 2 +- 29 files changed, 357 insertions(+), 84 deletions(-) rename SatelliteProvider.Common/DTO/{RegionStatus.cs => RegionStatusResponse.cs} (74%) create mode 100644 SatelliteProvider.Common/Enums/RegionStatus.cs create mode 100644 SatelliteProvider.Common/Enums/RoutePointType.cs create mode 100644 SatelliteProvider.DataAccess/TypeHandlers/EnumStringTypeHandler.cs create mode 100644 SatelliteProvider.Tests/AcceptanceCriteriaRT2Tests.cs create mode 100644 SatelliteProvider.Tests/EnumStringTypeHandlerTests.cs rename _docs/02_tasks/{todo => done}/AZ-370_refactor_status_pointtype_enums.md (100%) diff --git a/SatelliteProvider.Api/Program.cs b/SatelliteProvider.Api/Program.cs index 2ad3ef4..205336b 100644 --- a/SatelliteProvider.Api/Program.cs +++ b/SatelliteProvider.Api/Program.cs @@ -6,6 +6,7 @@ using SatelliteProvider.Api.DTOs; using SatelliteProvider.Api.Swagger; using SatelliteProvider.DataAccess; using SatelliteProvider.DataAccess.Repositories; +using SatelliteProvider.DataAccess.TypeHandlers; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; using SatelliteProvider.Common.Interfaces; @@ -22,6 +23,8 @@ builder.Host.UseSerilog((context, configuration) => var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); +DapperEnumTypeHandlers.RegisterAll(); + builder.Services.Configure(builder.Configuration.GetSection("MapConfig")); builder.Services.Configure(builder.Configuration.GetSection("StorageConfig")); builder.Services.Configure(builder.Configuration.GetSection("ProcessingConfig")); @@ -58,6 +61,8 @@ builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; options.SerializerOptions.PropertyNameCaseInsensitive = true; + options.SerializerOptions.Converters.Add( + new System.Text.Json.Serialization.JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy.CamelCase)); }); builder.Services.AddEndpointsApiExplorer(); diff --git a/SatelliteProvider.Common/DTO/RegionStatus.cs b/SatelliteProvider.Common/DTO/RegionStatusResponse.cs similarity index 74% rename from SatelliteProvider.Common/DTO/RegionStatus.cs rename to SatelliteProvider.Common/DTO/RegionStatusResponse.cs index e7e831e..6874426 100644 --- a/SatelliteProvider.Common/DTO/RegionStatus.cs +++ b/SatelliteProvider.Common/DTO/RegionStatusResponse.cs @@ -1,9 +1,11 @@ +using SatelliteProvider.Common.Enums; + namespace SatelliteProvider.Common.DTO; -public class RegionStatus +public class RegionStatusResponse { public Guid Id { get; set; } - public string Status { get; set; } = string.Empty; + public RegionStatus Status { get; set; } public string? CsvFilePath { get; set; } public string? SummaryFilePath { get; set; } public int TilesDownloaded { get; set; } @@ -11,4 +13,3 @@ public class RegionStatus public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } - diff --git a/SatelliteProvider.Common/DTO/RoutePointDto.cs b/SatelliteProvider.Common/DTO/RoutePointDto.cs index 39b654a..9f79eae 100644 --- a/SatelliteProvider.Common/DTO/RoutePointDto.cs +++ b/SatelliteProvider.Common/DTO/RoutePointDto.cs @@ -1,12 +1,13 @@ +using SatelliteProvider.Common.Enums; + namespace SatelliteProvider.Common.DTO; public class RoutePointDto { public double Latitude { get; set; } public double Longitude { get; set; } - public string PointType { get; set; } = string.Empty; + public RoutePointType PointType { get; set; } public int SequenceNumber { get; set; } public int SegmentIndex { get; set; } public double? DistanceFromPrevious { get; set; } } - diff --git a/SatelliteProvider.Common/Enums/RegionStatus.cs b/SatelliteProvider.Common/Enums/RegionStatus.cs new file mode 100644 index 0000000..eef62cc --- /dev/null +++ b/SatelliteProvider.Common/Enums/RegionStatus.cs @@ -0,0 +1,9 @@ +namespace SatelliteProvider.Common.Enums; + +public enum RegionStatus +{ + Queued, + Processing, + Completed, + Failed, +} diff --git a/SatelliteProvider.Common/Enums/RoutePointType.cs b/SatelliteProvider.Common/Enums/RoutePointType.cs new file mode 100644 index 0000000..3b55c3b --- /dev/null +++ b/SatelliteProvider.Common/Enums/RoutePointType.cs @@ -0,0 +1,9 @@ +namespace SatelliteProvider.Common.Enums; + +public enum RoutePointType +{ + Start, + End, + Action, + Intermediate, +} diff --git a/SatelliteProvider.Common/Interfaces/IRegionService.cs b/SatelliteProvider.Common/Interfaces/IRegionService.cs index e375466..e6ace98 100644 --- a/SatelliteProvider.Common/Interfaces/IRegionService.cs +++ b/SatelliteProvider.Common/Interfaces/IRegionService.cs @@ -4,8 +4,8 @@ namespace SatelliteProvider.Common.Interfaces; public interface IRegionService { - Task RequestRegionAsync(Guid id, double latitude, double longitude, double sizeMeters, int zoomLevel, bool stitchTiles = false); - Task GetRegionStatusAsync(Guid id); + Task RequestRegionAsync(Guid id, double latitude, double longitude, double sizeMeters, int zoomLevel, bool stitchTiles = false); + Task GetRegionStatusAsync(Guid id); Task ProcessRegionAsync(Guid id, CancellationToken cancellationToken = default); } diff --git a/SatelliteProvider.DataAccess/Models/RegionEntity.cs b/SatelliteProvider.DataAccess/Models/RegionEntity.cs index 548b156..6da2e59 100644 --- a/SatelliteProvider.DataAccess/Models/RegionEntity.cs +++ b/SatelliteProvider.DataAccess/Models/RegionEntity.cs @@ -1,3 +1,5 @@ +using SatelliteProvider.Common.Enums; + namespace SatelliteProvider.DataAccess.Models; public class RegionEntity @@ -7,7 +9,7 @@ public class RegionEntity public double Longitude { get; set; } public double SizeMeters { get; set; } public int ZoomLevel { get; set; } - public string Status { get; set; } = string.Empty; + public RegionStatus Status { get; set; } public string? CsvFilePath { get; set; } public string? SummaryFilePath { get; set; } public int TilesDownloaded { get; set; } diff --git a/SatelliteProvider.DataAccess/Models/RoutePointEntity.cs b/SatelliteProvider.DataAccess/Models/RoutePointEntity.cs index 7fb6a28..be27841 100644 --- a/SatelliteProvider.DataAccess/Models/RoutePointEntity.cs +++ b/SatelliteProvider.DataAccess/Models/RoutePointEntity.cs @@ -1,3 +1,5 @@ +using SatelliteProvider.Common.Enums; + namespace SatelliteProvider.DataAccess.Models; public class RoutePointEntity @@ -7,7 +9,7 @@ public class RoutePointEntity public int SequenceNumber { get; set; } public double Latitude { get; set; } public double Longitude { get; set; } - public string PointType { get; set; } = string.Empty; + public RoutePointType PointType { get; set; } public int SegmentIndex { get; set; } public double? DistanceFromPrevious { get; set; } public DateTime CreatedAt { get; set; } diff --git a/SatelliteProvider.DataAccess/Repositories/IRegionRepository.cs b/SatelliteProvider.DataAccess/Repositories/IRegionRepository.cs index ee2d155..53ec89e 100644 --- a/SatelliteProvider.DataAccess/Repositories/IRegionRepository.cs +++ b/SatelliteProvider.DataAccess/Repositories/IRegionRepository.cs @@ -1,3 +1,4 @@ +using SatelliteProvider.Common.Enums; using SatelliteProvider.DataAccess.Models; namespace SatelliteProvider.DataAccess.Repositories; @@ -5,7 +6,7 @@ namespace SatelliteProvider.DataAccess.Repositories; public interface IRegionRepository { Task GetByIdAsync(Guid id); - Task> GetByStatusAsync(string status); + Task> GetByStatusAsync(RegionStatus status); Task InsertAsync(RegionEntity region); Task UpdateAsync(RegionEntity region); Task DeleteAsync(Guid id); diff --git a/SatelliteProvider.DataAccess/Repositories/RegionRepository.cs b/SatelliteProvider.DataAccess/Repositories/RegionRepository.cs index c9fc26e..6f79e72 100644 --- a/SatelliteProvider.DataAccess/Repositories/RegionRepository.cs +++ b/SatelliteProvider.DataAccess/Repositories/RegionRepository.cs @@ -1,6 +1,7 @@ using Dapper; using Microsoft.Extensions.Logging; using Npgsql; +using SatelliteProvider.Common.Enums; using SatelliteProvider.DataAccess.Models; namespace SatelliteProvider.DataAccess.Repositories; @@ -33,7 +34,7 @@ public class RegionRepository : IRegionRepository return region; } - public async Task> GetByStatusAsync(string status) + public async Task> GetByStatusAsync(RegionStatus status) { using var connection = new NpgsqlConnection(_connectionString); const string sql = @" diff --git a/SatelliteProvider.DataAccess/SatelliteProvider.DataAccess.csproj b/SatelliteProvider.DataAccess/SatelliteProvider.DataAccess.csproj index b403f78..e30dce4 100644 --- a/SatelliteProvider.DataAccess/SatelliteProvider.DataAccess.csproj +++ b/SatelliteProvider.DataAccess/SatelliteProvider.DataAccess.csproj @@ -14,6 +14,10 @@ + + + + diff --git a/SatelliteProvider.DataAccess/TypeHandlers/EnumStringTypeHandler.cs b/SatelliteProvider.DataAccess/TypeHandlers/EnumStringTypeHandler.cs new file mode 100644 index 0000000..221acb8 --- /dev/null +++ b/SatelliteProvider.DataAccess/TypeHandlers/EnumStringTypeHandler.cs @@ -0,0 +1,51 @@ +using System.Data; +using Dapper; +using SatelliteProvider.Common.Enums; + +namespace SatelliteProvider.DataAccess.TypeHandlers; + +public class EnumStringTypeHandler : SqlMapper.TypeHandler where T : struct, Enum +{ + public override T Parse(object value) + { + if (value is null || value is DBNull) + { + throw new DataException($"Cannot parse null DB value into enum {typeof(T).Name}"); + } + + var s = value as string ?? value.ToString(); + if (string.IsNullOrEmpty(s)) + { + throw new DataException($"Cannot parse empty DB value into enum {typeof(T).Name}"); + } + + if (!Enum.TryParse(s, ignoreCase: true, out var parsed) || !Enum.IsDefined(parsed)) + { + throw new DataException($"DB value '{s}' is not a defined member of enum {typeof(T).Name}"); + } + + return parsed; + } + + public override void SetValue(IDbDataParameter parameter, T value) + { + parameter.Value = value.ToString().ToLowerInvariant(); + parameter.DbType = DbType.String; + } +} + +public static class DapperEnumTypeHandlers +{ + private static int _registered; + + public static void RegisterAll() + { + if (Interlocked.Exchange(ref _registered, 1) == 1) + { + return; + } + + SqlMapper.AddTypeHandler(new EnumStringTypeHandler()); + SqlMapper.AddTypeHandler(new EnumStringTypeHandler()); + } +} diff --git a/SatelliteProvider.Services.RegionProcessing/RegionService.cs b/SatelliteProvider.Services.RegionProcessing/RegionService.cs index 220dcae..e5db792 100644 --- a/SatelliteProvider.Services.RegionProcessing/RegionService.cs +++ b/SatelliteProvider.Services.RegionProcessing/RegionService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; +using SatelliteProvider.Common.Enums; using SatelliteProvider.Common.Exceptions; using SatelliteProvider.Common.Imaging; using SatelliteProvider.Common.Interfaces; @@ -38,7 +39,7 @@ public class RegionService : IRegionService _logger = logger; } - public async Task RequestRegionAsync(Guid id, double latitude, double longitude, double sizeMeters, int zoomLevel, bool stitchTiles = false) + public async Task RequestRegionAsync(Guid id, double latitude, double longitude, double sizeMeters, int zoomLevel, bool stitchTiles = false) { // AZ-362: idempotent POST contract. A retried POST with the same caller-supplied // Id returns the existing region instead of bubbling a unique-key violation. @@ -47,7 +48,7 @@ public class RegionService : IRegionService { _logger.LogInformation( "Idempotent region POST: id {RegionId} already exists with status {Status}; returning existing resource without re-enqueueing", - id, existing.Status); + id, existing.Status.ToString().ToLowerInvariant()); return MapToStatus(existing); } @@ -60,7 +61,7 @@ public class RegionService : IRegionService SizeMeters = sizeMeters, ZoomLevel = zoomLevel, StitchTiles = stitchTiles, - Status = "queued", + Status = RegionStatus.Queued, TilesDownloaded = 0, TilesReused = 0, CreatedAt = now, @@ -84,7 +85,7 @@ public class RegionService : IRegionService return MapToStatus(region); } - public async Task GetRegionStatusAsync(Guid id) + public async Task GetRegionStatusAsync(Guid id) { var region = await _regionRepository.GetByIdAsync(id); return region != null ? MapToStatus(region) : null; @@ -101,7 +102,7 @@ public class RegionService : IRegionService return; } - region.Status = "processing"; + region.Status = RegionStatus.Processing; region.UpdatedAt = DateTime.UtcNow; await _regionRepository.UpdateAsync(region); @@ -151,7 +152,7 @@ public class RegionService : IRegionService await GenerateSummaryFileAsync(summaryPath, id, region, tiles, tilesDownloaded, tilesReused, stitchedImagePath, processingStartTime, linkedCts.Token, errorMessage); - region.Status = "completed"; + region.Status = RegionStatus.Completed; region.CsvFilePath = csvPath; region.SummaryFilePath = summaryPath; region.TilesDownloaded = tilesDownloaded; @@ -182,7 +183,7 @@ public class RegionService : IRegionService int tilesReused, string errorMessage) { - region.Status = "failed"; + region.Status = RegionStatus.Failed; region.UpdatedAt = DateTime.UtcNow; try @@ -311,7 +312,7 @@ public class RegionService : IRegionService summary.AppendLine($"Center: {region.Latitude:F6}, {region.Longitude:F6}"); summary.AppendLine($"Size: {region.SizeMeters:F0} meters"); summary.AppendLine($"Zoom Level: {region.ZoomLevel}"); - summary.AppendLine($"Status: {region.Status}"); + summary.AppendLine($"Status: {region.Status.ToString().ToLowerInvariant()}"); summary.AppendLine(); if (!string.IsNullOrEmpty(errorMessage)) @@ -328,7 +329,7 @@ public class RegionService : IRegionService summary.AppendLine($"- Processing Time: {processingTime:F2} seconds"); summary.AppendLine($"- Started: {startTime:yyyy-MM-dd HH:mm:ss} UTC"); - if (region.Status == "completed") + if (region.Status == RegionStatus.Completed) { summary.AppendLine($"- Completed: {endTime:yyyy-MM-dd HH:mm:ss} UTC"); } @@ -356,9 +357,9 @@ public class RegionService : IRegionService await File.WriteAllTextAsync(filePath, summary.ToString(), cancellationToken); } - private static RegionStatus MapToStatus(RegionEntity region) + private static RegionStatusResponse MapToStatus(RegionEntity region) { - return new RegionStatus + return new RegionStatusResponse { Id = region.Id, Status = region.Status, diff --git a/SatelliteProvider.Services.RouteManagement/RoutePointGraphBuilder.cs b/SatelliteProvider.Services.RouteManagement/RoutePointGraphBuilder.cs index e130b0d..9e6d67f 100644 --- a/SatelliteProvider.Services.RouteManagement/RoutePointGraphBuilder.cs +++ b/SatelliteProvider.Services.RouteManagement/RoutePointGraphBuilder.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Options; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; +using SatelliteProvider.Common.Enums; using SatelliteProvider.Common.Utils; namespace SatelliteProvider.Services.RouteManagement; @@ -48,7 +49,7 @@ public class RoutePointGraphBuilder { Latitude = current.Latitude, Longitude = current.Longitude, - PointType = isStart ? "start" : (isEnd ? "end" : "action"), + PointType = isStart ? RoutePointType.Start : (isEnd ? RoutePointType.End : RoutePointType.Action), SequenceNumber = sequenceNumber++, SegmentIndex = segmentIndex, DistanceFromPrevious = distanceFromPrevious, @@ -74,7 +75,7 @@ public class RoutePointGraphBuilder { Latitude = intermediateGeo.Lat, Longitude = intermediateGeo.Lon, - PointType = "intermediate", + PointType = RoutePointType.Intermediate, SequenceNumber = sequenceNumber++, SegmentIndex = segmentIndex, DistanceFromPrevious = distFromPrev, diff --git a/SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs b/SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs index bb710bd..5345295 100644 --- a/SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs +++ b/SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; +using SatelliteProvider.Common.Enums; using SatelliteProvider.Common.Interfaces; using SatelliteProvider.Common.Utils; using SatelliteProvider.DataAccess.Models; @@ -146,9 +147,9 @@ public class RouteProcessingService : BackgroundService } } - var completedRegions = regions.Where(r => r.Status == "completed").ToList(); - var failedRegions = regions.Where(r => r.Status == "failed").ToList(); - var processingRegions = regions.Where(r => r.Status == "queued" || r.Status == "processing").ToList(); + var completedRegions = regions.Where(r => r.Status == RegionStatus.Completed).ToList(); + var failedRegions = regions.Where(r => r.Status == RegionStatus.Failed).ToList(); + var processingRegions = regions.Where(r => r.Status == RegionStatus.Queued || r.Status == RegionStatus.Processing).ToList(); var completedRoutePointRegions = completedRegions.Where(r => !geofenceRegionIdsList.Contains(r.Id)).ToList(); var completedGeofenceRegions = completedRegions.Where(r => geofenceRegionIdsList.Contains(r.Id)).ToList(); diff --git a/SatelliteProvider.Tests/AcceptanceCriteriaRT2Tests.cs b/SatelliteProvider.Tests/AcceptanceCriteriaRT2Tests.cs new file mode 100644 index 0000000..3777c9e --- /dev/null +++ b/SatelliteProvider.Tests/AcceptanceCriteriaRT2Tests.cs @@ -0,0 +1,40 @@ +using FluentAssertions; + +namespace SatelliteProvider.Tests; + +public class AcceptanceCriteriaRT2Tests +{ + [Fact] + public void RT2_LinesAllFourPointTypes_AZ370_AC3() + { + var path = LocateAcceptanceCriteriaMd(); + if (path is null) + { + Assert.Fail("acceptance_criteria.md not found from test runtime — repo layout drift"); + } + + var content = File.ReadAllText(path); + var rt2Line = content.Split('\n').FirstOrDefault(line => line.Contains("| RT2 |")); + + rt2Line.Should().NotBeNull("RT2 row must exist in the Route Management section"); + rt2Line!.Should().Contain("\"start\""); + rt2Line.Should().Contain("\"end\""); + rt2Line.Should().Contain("\"action\""); + rt2Line.Should().Contain("\"intermediate\""); + } + + private static string? LocateAcceptanceCriteriaMd() + { + var dir = new DirectoryInfo(Directory.GetCurrentDirectory()); + while (dir is not null) + { + var candidate = Path.Combine(dir.FullName, "_docs", "00_problem", "acceptance_criteria.md"); + if (File.Exists(candidate)) + { + return candidate; + } + dir = dir.Parent; + } + return null; + } +} diff --git a/SatelliteProvider.Tests/EnumStringTypeHandlerTests.cs b/SatelliteProvider.Tests/EnumStringTypeHandlerTests.cs new file mode 100644 index 0000000..58b2c06 --- /dev/null +++ b/SatelliteProvider.Tests/EnumStringTypeHandlerTests.cs @@ -0,0 +1,140 @@ +using System.Data; +using FluentAssertions; +using Npgsql; +using SatelliteProvider.Common.Enums; +using SatelliteProvider.DataAccess.TypeHandlers; + +namespace SatelliteProvider.Tests; + +public class EnumStringTypeHandlerTests +{ + [Theory] + [InlineData(RegionStatus.Queued, "queued")] + [InlineData(RegionStatus.Processing, "processing")] + [InlineData(RegionStatus.Completed, "completed")] + [InlineData(RegionStatus.Failed, "failed")] + public void SetValue_RegionStatus_WritesLowercaseString_AZ370_AC1(RegionStatus value, string expected) + { + var handler = new EnumStringTypeHandler(); + var param = new NpgsqlParameter(); + + handler.SetValue(param, value); + + param.Value.Should().Be(expected); + param.DbType.Should().Be(DbType.String); + } + + [Theory] + [InlineData(RoutePointType.Start, "start")] + [InlineData(RoutePointType.End, "end")] + [InlineData(RoutePointType.Action, "action")] + [InlineData(RoutePointType.Intermediate, "intermediate")] + public void SetValue_RoutePointType_WritesLowercaseString_AZ370_AC1(RoutePointType value, string expected) + { + var handler = new EnumStringTypeHandler(); + var param = new NpgsqlParameter(); + + handler.SetValue(param, value); + + param.Value.Should().Be(expected); + param.DbType.Should().Be(DbType.String); + } + + [Theory] + [InlineData("queued", RegionStatus.Queued)] + [InlineData("processing", RegionStatus.Processing)] + [InlineData("completed", RegionStatus.Completed)] + [InlineData("failed", RegionStatus.Failed)] + [InlineData("Completed", RegionStatus.Completed)] + public void Parse_RegionStatus_AcceptsAnyCase_AZ370_AC2(string raw, RegionStatus expected) + { + var handler = new EnumStringTypeHandler(); + + var result = handler.Parse(raw); + + result.Should().Be(expected); + } + + [Theory] + [InlineData("start", RoutePointType.Start)] + [InlineData("end", RoutePointType.End)] + [InlineData("action", RoutePointType.Action)] + [InlineData("intermediate", RoutePointType.Intermediate)] + public void Parse_RoutePointType_AcceptsLowercase_AZ370_AC2(string raw, RoutePointType expected) + { + var handler = new EnumStringTypeHandler(); + + var result = handler.Parse(raw); + + result.Should().Be(expected); + } + + [Theory] + [InlineData(RegionStatus.Queued)] + [InlineData(RegionStatus.Processing)] + [InlineData(RegionStatus.Completed)] + [InlineData(RegionStatus.Failed)] + public void RoundTrip_RegionStatus_PreservesValue_AZ370_AC2(RegionStatus value) + { + var handler = new EnumStringTypeHandler(); + var param = new NpgsqlParameter(); + handler.SetValue(param, value); + + var roundTripped = handler.Parse(param.Value!); + + roundTripped.Should().Be(value); + } + + [Theory] + [InlineData(RoutePointType.Start)] + [InlineData(RoutePointType.End)] + [InlineData(RoutePointType.Action)] + [InlineData(RoutePointType.Intermediate)] + public void RoundTrip_RoutePointType_PreservesValue_AZ370_AC2(RoutePointType value) + { + var handler = new EnumStringTypeHandler(); + var param = new NpgsqlParameter(); + handler.SetValue(param, value); + + var roundTripped = handler.Parse(param.Value!); + + roundTripped.Should().Be(value); + } + + [Fact] + public void Parse_NullValue_ThrowsDataException() + { + var handler = new EnumStringTypeHandler(); + + Action act = () => handler.Parse(null!); + + act.Should().Throw(); + } + + [Fact] + public void Parse_DbNullValue_ThrowsDataException() + { + var handler = new EnumStringTypeHandler(); + + Action act = () => handler.Parse(DBNull.Value); + + act.Should().Throw(); + } + + [Fact] + public void Parse_UnknownString_ThrowsDataException() + { + var handler = new EnumStringTypeHandler(); + + Action act = () => handler.Parse("not-a-status"); + + act.Should().Throw(); + } + + [Fact] + public void RegisterAll_IsIdempotent() + { + DapperEnumTypeHandlers.RegisterAll(); + DapperEnumTypeHandlers.RegisterAll(); + } +} diff --git a/SatelliteProvider.Tests/RegionServiceTests.cs b/SatelliteProvider.Tests/RegionServiceTests.cs index 40cc808..f7e823b 100644 --- a/SatelliteProvider.Tests/RegionServiceTests.cs +++ b/SatelliteProvider.Tests/RegionServiceTests.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using Moq; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; +using SatelliteProvider.Common.Enums; using SatelliteProvider.Common.Exceptions; using SatelliteProvider.Common.Interfaces; using SatelliteProvider.DataAccess.Models; @@ -57,10 +58,10 @@ public class RegionServiceTests : IDisposable // Assert status.Id.Should().Be(id); - status.Status.Should().Be("queued"); + status.Status.Should().Be(RegionStatus.Queued); regionRepo.Verify(r => r.InsertAsync(It.Is(re => re.Id == id && - re.Status == "queued" && + re.Status == RegionStatus.Queued && re.SizeMeters == 200 && re.ZoomLevel == 18)), Times.Once); queue.Verify(q => q.EnqueueAsync(It.Is(rr => @@ -83,7 +84,7 @@ public class RegionServiceTests : IDisposable SizeMeters = 200, ZoomLevel = 18, StitchTiles = false, - Status = "processing", + Status = RegionStatus.Processing, TilesDownloaded = 5, TilesReused = 3, CreatedAt = DateTime.UtcNow.AddMinutes(-5), @@ -100,7 +101,7 @@ public class RegionServiceTests : IDisposable // Assert status.Id.Should().Be(id); - status.Status.Should().Be("processing", "AZ-362: returns the existing region's current status, not 'queued'"); + status.Status.Should().Be(RegionStatus.Processing, "AZ-362: returns the existing region's current status, not 'queued'"); regionRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Never, "AZ-362: duplicate Id must not re-insert"); queue.VerifyNoOtherCalls(); @@ -122,7 +123,7 @@ public class RegionServiceTests : IDisposable Longitude = 37.647063, SizeMeters = 200, ZoomLevel = 18, - Status = "queued", + Status = RegionStatus.Queued, StitchTiles = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, @@ -130,7 +131,7 @@ public class RegionServiceTests : IDisposable regionRepo.Setup(r => r.GetByIdAsync(id)).ReturnsAsync(entity); - var capturedStatuses = new List(); + var capturedStatuses = new List(); regionRepo .Setup(r => r.UpdateAsync(It.IsAny())) .Callback(e => capturedStatuses.Add(e.Status)) @@ -163,8 +164,8 @@ public class RegionServiceTests : IDisposable await service.ProcessRegionAsync(id); // Assert - capturedStatuses.Should().ContainInOrder("processing", "completed"); - entity.Status.Should().Be("completed"); + capturedStatuses.Should().ContainInOrder(RegionStatus.Processing, RegionStatus.Completed); + entity.Status.Should().Be(RegionStatus.Completed); entity.CsvFilePath.Should().NotBeNullOrEmpty(); entity.SummaryFilePath.Should().NotBeNullOrEmpty(); entity.TilesDownloaded.Should().Be(1); @@ -204,7 +205,7 @@ public class RegionServiceTests : IDisposable Longitude = 37.647063, SizeMeters = 200, ZoomLevel = 18, - Status = "queued", + Status = RegionStatus.Queued, StitchTiles = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, @@ -212,7 +213,7 @@ public class RegionServiceTests : IDisposable regionRepo.Setup(r => r.GetByIdAsync(id)).ReturnsAsync(entity); - var capturedStatuses = new List(); + var capturedStatuses = new List(); regionRepo .Setup(r => r.UpdateAsync(It.IsAny())) .Callback(e => capturedStatuses.Add(e.Status)) @@ -233,8 +234,8 @@ public class RegionServiceTests : IDisposable await service.ProcessRegionAsync(id); // Assert - capturedStatuses.Should().ContainInOrder("processing", "failed"); - entity.Status.Should().Be("failed"); + capturedStatuses.Should().ContainInOrder(RegionStatus.Processing, RegionStatus.Failed); + entity.Status.Should().Be(RegionStatus.Failed); entity.SummaryFilePath.Should().NotBeNullOrEmpty(); File.Exists(entity.SummaryFilePath!).Should().BeTrue(); File.ReadAllText(entity.SummaryFilePath!).Should().Contain("Rate limit exceeded"); @@ -256,7 +257,7 @@ public class RegionServiceTests : IDisposable Longitude = 37.647063, SizeMeters = 500, ZoomLevel = 18, - Status = "queued", + Status = RegionStatus.Queued, StitchTiles = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, @@ -293,7 +294,7 @@ public class RegionServiceTests : IDisposable await service.ProcessRegionAsync(id); // Assert - entity.Status.Should().Be("completed"); + entity.Status.Should().Be(RegionStatus.Completed); var stitchedPath = Path.Combine(_readyDir, $"region_{id}_stitched.jpg"); File.Exists(stitchedPath).Should().BeTrue("stitched image must exist when stitchTiles=true"); } @@ -305,7 +306,7 @@ public class RegionServiceTests : IDisposable var regionRepo = new Mock(); var id = Guid.NewGuid(); regionRepo.Setup(r => r.GetByIdAsync(id)) - .ReturnsAsync(new RegionEntity { Id = id, Status = "completed", TilesDownloaded = 5, TilesReused = 2 }); + .ReturnsAsync(new RegionEntity { Id = id, Status = RegionStatus.Completed, TilesDownloaded = 5, TilesReused = 2 }); var service = BuildService(regionRepo, new Mock(), new Mock()); // Act @@ -313,7 +314,7 @@ public class RegionServiceTests : IDisposable // Assert result.Should().NotBeNull(); - result!.Status.Should().Be("completed"); + result!.Status.Should().Be(RegionStatus.Completed); result.TilesDownloaded.Should().Be(5); result.TilesReused.Should().Be(2); } diff --git a/SatelliteProvider.Tests/RoutePointGraphBuilderTests.cs b/SatelliteProvider.Tests/RoutePointGraphBuilderTests.cs index e1f5013..856efc2 100644 --- a/SatelliteProvider.Tests/RoutePointGraphBuilderTests.cs +++ b/SatelliteProvider.Tests/RoutePointGraphBuilderTests.cs @@ -2,6 +2,7 @@ using FluentAssertions; using Microsoft.Extensions.Options; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; +using SatelliteProvider.Common.Enums; using SatelliteProvider.Common.Utils; using SatelliteProvider.Services.RouteManagement; using SatelliteProvider.Tests.Fixtures; @@ -26,10 +27,10 @@ public class RoutePointGraphBuilderTests var graph = sut.Build(input); - graph.Points.First().PointType.Should().Be("start"); - graph.Points.Last().PointType.Should().Be("end"); + graph.Points.First().PointType.Should().Be(RoutePointType.Start); + graph.Points.Last().PointType.Should().Be(RoutePointType.End); graph.Points.Skip(1).Take(graph.Points.Count - 2) - .Should().OnlyContain(p => p.PointType == "intermediate"); + .Should().OnlyContain(p => p.PointType == RoutePointType.Intermediate); } [Fact] @@ -60,10 +61,10 @@ public class RoutePointGraphBuilderTests var graph = sut.Build(input); - graph.Points.Count(p => p.PointType == "start").Should().Be(1); - graph.Points.Count(p => p.PointType == "end").Should().Be(1); - graph.Points.Count(p => p.PointType == "action").Should().Be(8); - graph.Points.Should().Contain(p => p.PointType == "intermediate"); + graph.Points.Count(p => p.PointType == RoutePointType.Start).Should().Be(1); + graph.Points.Count(p => p.PointType == RoutePointType.End).Should().Be(1); + graph.Points.Count(p => p.PointType == RoutePointType.Action).Should().Be(8); + graph.Points.Should().Contain(p => p.PointType == RoutePointType.Intermediate); } [Fact] diff --git a/SatelliteProvider.Tests/RouteResponseMapperTests.cs b/SatelliteProvider.Tests/RouteResponseMapperTests.cs index b68ca19..a574e67 100644 --- a/SatelliteProvider.Tests/RouteResponseMapperTests.cs +++ b/SatelliteProvider.Tests/RouteResponseMapperTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using SatelliteProvider.Common.DTO; +using SatelliteProvider.Common.Enums; using SatelliteProvider.DataAccess.Models; using SatelliteProvider.Services.RouteManagement; @@ -33,8 +34,8 @@ public class RouteResponseMapperTests var entity = BuildEntity(id); var dtos = new List { - new() { Latitude = 1, Longitude = 2, PointType = "start", SequenceNumber = 0, SegmentIndex = 0 }, - new() { Latitude = 3, Longitude = 4, PointType = "end", SequenceNumber = 1, SegmentIndex = 1, DistanceFromPrevious = 100.0 }, + new() { Latitude = 1, Longitude = 2, PointType = RoutePointType.Start, SequenceNumber = 0, SegmentIndex = 0 }, + new() { Latitude = 3, Longitude = 4, PointType = RoutePointType.End, SequenceNumber = 1, SegmentIndex = 1, DistanceFromPrevious = 100.0 }, }; var sut = new RouteResponseMapper(); @@ -65,17 +66,17 @@ public class RouteResponseMapperTests var entity = BuildEntity(id); var pointEntities = new List { - new() { Id = Guid.NewGuid(), RouteId = id, SequenceNumber = 0, Latitude = 1, Longitude = 2, PointType = "start", SegmentIndex = 0 }, - new() { Id = Guid.NewGuid(), RouteId = id, SequenceNumber = 1, Latitude = 3, Longitude = 4, PointType = "end", SegmentIndex = 1, DistanceFromPrevious = 100.0 }, + new() { Id = Guid.NewGuid(), RouteId = id, SequenceNumber = 0, Latitude = 1, Longitude = 2, PointType = RoutePointType.Start, SegmentIndex = 0 }, + new() { Id = Guid.NewGuid(), RouteId = id, SequenceNumber = 1, Latitude = 3, Longitude = 4, PointType = RoutePointType.End, SegmentIndex = 1, DistanceFromPrevious = 100.0 }, }; var sut = new RouteResponseMapper(); var response = sut.Map(entity, pointEntities); response.Points.Should().HaveCount(2); - response.Points[0].PointType.Should().Be("start"); + response.Points[0].PointType.Should().Be(RoutePointType.Start); response.Points[0].Latitude.Should().Be(1); - response.Points[1].PointType.Should().Be("end"); + response.Points[1].PointType.Should().Be(RoutePointType.End); response.Points[1].DistanceFromPrevious.Should().Be(100.0); } diff --git a/SatelliteProvider.Tests/RouteServiceTests.cs b/SatelliteProvider.Tests/RouteServiceTests.cs index 72d7fc4..da36b96 100644 --- a/SatelliteProvider.Tests/RouteServiceTests.cs +++ b/SatelliteProvider.Tests/RouteServiceTests.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using Moq; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; +using SatelliteProvider.Common.Enums; using SatelliteProvider.Common.Interfaces; using SatelliteProvider.Common.Utils; using SatelliteProvider.DataAccess.Models; @@ -59,8 +60,8 @@ public class RouteServiceTests }; var existingPoints = new List { - new() { Id = Guid.NewGuid(), RouteId = existingId, SequenceNumber = 0, Latitude = 47.46, Longitude = 37.64, PointType = "start", SegmentIndex = 0 }, - new() { Id = Guid.NewGuid(), RouteId = existingId, SequenceNumber = 1, Latitude = 47.47, Longitude = 37.65, PointType = "end", SegmentIndex = 0 }, + new() { Id = Guid.NewGuid(), RouteId = existingId, SequenceNumber = 0, Latitude = 47.46, Longitude = 37.64, PointType = RoutePointType.Start, SegmentIndex = 0 }, + new() { Id = Guid.NewGuid(), RouteId = existingId, SequenceNumber = 1, Latitude = 47.47, Longitude = 37.65, PointType = RoutePointType.End, SegmentIndex = 0 }, }; var routeRepo = new Mock(MockBehavior.Strict); routeRepo.Setup(r => r.GetByIdAsync(existingId)).ReturnsAsync(existingEntity); @@ -125,9 +126,9 @@ public class RouteServiceTests var result = await service.CreateRouteAsync(request); // Assert - result.Points.First().PointType.Should().Be("start", "first user-supplied point"); - result.Points.Last().PointType.Should().Be("end", "last user-supplied point"); - result.Points.Skip(1).Take(result.Points.Count - 2).Should().OnlyContain(p => p.PointType == "intermediate", + result.Points.First().PointType.Should().Be(RoutePointType.Start, "first user-supplied point"); + result.Points.Last().PointType.Should().Be(RoutePointType.End, "last user-supplied point"); + result.Points.Skip(1).Take(result.Points.Count - 2).Should().OnlyContain(p => p.PointType == RoutePointType.Intermediate, "every middle point in a 2-point route is interpolated"); } @@ -142,10 +143,10 @@ public class RouteServiceTests var result = await service.CreateRouteAsync(request); // Assert - result.Points.Count(p => p.PointType == "start").Should().Be(1); - result.Points.Count(p => p.PointType == "end").Should().Be(1); - result.Points.Count(p => p.PointType == "action").Should().Be(8, "8 middle user-supplied waypoints in a 10-point route"); - result.Points.Should().Contain(p => p.PointType == "intermediate", "long route always interpolates"); + result.Points.Count(p => p.PointType == RoutePointType.Start).Should().Be(1); + result.Points.Count(p => p.PointType == RoutePointType.End).Should().Be(1); + result.Points.Count(p => p.PointType == RoutePointType.Action).Should().Be(8, "8 middle user-supplied waypoints in a 10-point route"); + result.Points.Should().Contain(p => p.PointType == RoutePointType.Intermediate, "long route always interpolates"); } [Fact] @@ -159,9 +160,9 @@ public class RouteServiceTests var result = await service.CreateRouteAsync(request); // Assert - result.Points.Count(p => p.PointType == "start").Should().Be(1); - result.Points.Count(p => p.PointType == "end").Should().Be(1); - result.Points.Count(p => p.PointType == "action").Should().Be(18, "18 middle user-supplied waypoints in a 20-point route"); + result.Points.Count(p => p.PointType == RoutePointType.Start).Should().Be(1); + result.Points.Count(p => p.PointType == RoutePointType.End).Should().Be(1); + result.Points.Count(p => p.PointType == RoutePointType.Action).Should().Be(18, "18 middle user-supplied waypoints in a 20-point route"); } [Fact] @@ -274,7 +275,7 @@ public class RouteServiceTests regionService .Setup(r => r.RequestRegionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), false)) - .ReturnsAsync(new RegionStatus { Status = "queued" }); + .ReturnsAsync(new RegionStatusResponse { Status = RegionStatus.Queued }); var service = BuildService(routeRepo, regionService); @@ -319,8 +320,8 @@ public class RouteServiceTests routeRepo.Setup(r => r.GetByIdAsync(id)).ReturnsAsync(routeEntity); routeRepo.Setup(r => r.GetRoutePointsAsync(id)).ReturnsAsync(new List { - new() { Id = Guid.NewGuid(), RouteId = id, SequenceNumber = 0, Latitude = 1, Longitude = 2, PointType = "start", SegmentIndex = 0 }, - new() { Id = Guid.NewGuid(), RouteId = id, SequenceNumber = 1, Latitude = 3, Longitude = 4, PointType = "end", SegmentIndex = 1 }, + new() { Id = Guid.NewGuid(), RouteId = id, SequenceNumber = 0, Latitude = 1, Longitude = 2, PointType = RoutePointType.Start, SegmentIndex = 0 }, + new() { Id = Guid.NewGuid(), RouteId = id, SequenceNumber = 1, Latitude = 3, Longitude = 4, PointType = RoutePointType.End, SegmentIndex = 1 }, }); var service = BuildService(routeRepo, new Mock()); @@ -332,8 +333,8 @@ public class RouteServiceTests result.Should().NotBeNull(); result!.Id.Should().Be(id); result.Points.Should().HaveCount(2); - result.Points[0].PointType.Should().Be("start"); - result.Points[1].PointType.Should().Be("end"); + result.Points[0].PointType.Should().Be(RoutePointType.Start); + result.Points[1].PointType.Should().Be(RoutePointType.End); } [Fact] diff --git a/_docs/00_problem/acceptance_criteria.md b/_docs/00_problem/acceptance_criteria.md index abf603c..3b84906 100644 --- a/_docs/00_problem/acceptance_criteria.md +++ b/_docs/00_problem/acceptance_criteria.md @@ -25,7 +25,7 @@ | # | Criterion | Measurable Value | Source | |---|----------|-----------------|--------| | RT1 | Points interpolated at correct interval | Intermediate points every ~200m along path | RouteService (InterpolatePoints) | -| RT2 | Point types correctly assigned | "original" for input waypoints, "intermediate" for generated | RoutePointEntity.PointType | +| RT2 | Point types correctly assigned | "start" for first waypoint, "end" for last waypoint, "action" for middle waypoints, "intermediate" for generated points | RoutePointEntity.PointType | | RT3 | Total distance calculated | Haversine sum matches within acceptable precision | RouteService.CreateRoute | | RT4 | Geofence filtering applied | Only points inside geofence rectangles generate regions | RouteService (point-in-rectangle check) | | RT5 | ZIP archive within size limit | ≤ 50 MB | RouteProcessingService ZIP generation | diff --git a/_docs/02_document/00_discovery.md b/_docs/02_document/00_discovery.md index 1c0e020..8ad77e1 100644 --- a/_docs/02_document/00_discovery.md +++ b/_docs/02_document/00_discovery.md @@ -34,7 +34,7 @@ satellite-provider/ │ │ ├── GeoPoint.cs │ │ ├── GeofencePolygon.cs │ │ ├── RegionRequest.cs -│ │ ├── RegionStatus.cs +│ │ ├── RegionStatusResponse.cs │ │ ├── RoutePoint.cs │ │ ├── RoutePointDto.cs │ │ ├── RouteResponse.cs diff --git a/_docs/02_document/04_verification_log.md b/_docs/02_document/04_verification_log.md index 063b92c..8900880 100644 --- a/_docs/02_document/04_verification_log.md +++ b/_docs/02_document/04_verification_log.md @@ -27,7 +27,7 @@ All classes, interfaces, and types mentioned in documentation were cross-referen - **Service implementations** (7/7): TileService, RegionService, RouteService, GoogleMapsDownloaderV2, RegionProcessingService, RouteProcessingService, RegionRequestQueue ✓ - **Repositories** (6/6): ITileRepository, IRegionRepository, IRouteRepository, TileRepository, RegionRepository, RouteRepository ✓ - **Config classes** (4/4): MapConfig, StorageConfig, ProcessingConfig, DatabaseConfig ✓ -- **DTOs** (10/10): GeoPoint, Direction, TileMetadata, RegionRequest, RegionStatus, RouteResponse, RoutePoint, RoutePointDto, CreateRouteRequest, GeofencePolygon ✓ +- **DTOs** (10/10): GeoPoint, Direction, TileMetadata, RegionRequest, RegionStatusResponse, RouteResponse, RoutePoint, RoutePointDto, CreateRouteRequest, GeofencePolygon ✓ - **Utilities** (1/1): GeoUtils ✓ - **Infrastructure** (1/1): DatabaseMigrator ✓ diff --git a/_docs/02_document/components/01_common/description.md b/_docs/02_document/components/01_common/description.md index 29fff73..ffd8e69 100644 --- a/_docs/02_document/components/01_common/description.md +++ b/_docs/02_document/components/01_common/description.md @@ -24,8 +24,8 @@ This component defines the service contracts that other components implement: ### Interface: IRegionService | Method | Input | Output | Async | Error Types | |--------|-------|--------|-------|-------------| -| `RequestRegionAsync` | id, lat, lon, sizeMeters, zoomLevel, stitchTiles | `RegionStatus` | Yes | Exception | -| `GetRegionStatusAsync` | Guid id | `RegionStatus?` | Yes | Exception | +| `RequestRegionAsync` | id, lat, lon, sizeMeters, zoomLevel, stitchTiles | `RegionStatusResponse` | Yes | Exception | +| `GetRegionStatusAsync` | Guid id | `RegionStatusResponse?` | Yes | Exception | | `ProcessRegionAsync` | Guid id, CancellationToken | void | Yes | RateLimitException, HttpRequestException, TimeoutException | ### Interface: IRouteService diff --git a/_docs/02_document/modules/common_interfaces.md b/_docs/02_document/modules/common_interfaces.md index dd525c7..23cf93a 100644 --- a/_docs/02_document/modules/common_interfaces.md +++ b/_docs/02_document/modules/common_interfaces.md @@ -13,8 +13,8 @@ Service contracts defining the application's core operations. Implementations li - `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task`: download one tile by lat/lon and persist (added in AZ-311) ### IRegionService -- `RequestRegionAsync(Guid id, double lat, double lon, double sizeMeters, int zoomLevel, bool stitchTiles) → Task`: creates a region record and enqueues for async processing -- `GetRegionStatusAsync(Guid id) → Task`: retrieves current status of a region request +- `RequestRegionAsync(Guid id, double lat, double lon, double sizeMeters, int zoomLevel, bool stitchTiles) → Task`: creates a region record and enqueues for async processing +- `GetRegionStatusAsync(Guid id) → Task`: retrieves current status of a region request - `ProcessRegionAsync(Guid id, CancellationToken) → Task`: executes tile downloading, CSV/summary generation, optional stitching ### IRouteService diff --git a/_docs/02_document/modules/services_region.md b/_docs/02_document/modules/services_region.md index 4a1e765..804da9a 100644 --- a/_docs/02_document/modules/services_region.md +++ b/_docs/02_document/modules/services_region.md @@ -6,8 +6,8 @@ End-to-end region processing pipeline: API request handling → queue → backgr ## Public Interface ### RegionService (implements IRegionService) -- `RequestRegionAsync(...)`: creates `RegionEntity` with status "queued", enqueues a `RegionRequest`, returns `RegionStatus` -- `GetRegionStatusAsync(Guid id)`: reads region record and maps to `RegionStatus` +- `RequestRegionAsync(...)`: creates `RegionEntity` with status `RegionStatus.Queued`, enqueues a `RegionRequest`, returns `RegionStatusResponse` +- `GetRegionStatusAsync(Guid id)`: reads region record and maps to `RegionStatusResponse` - `ProcessRegionAsync(Guid id, CancellationToken)`: the main processing pipeline — see Internal Logic ### RegionProcessingService (BackgroundService) @@ -71,7 +71,7 @@ Each sets status to "failed" and writes an error summary file. ## Data Models - Input: `RegionRequest` (queue message) -- Output: `RegionStatus` (API response), CSV files, summary files, stitched images +- Output: `RegionStatusResponse` (API response), CSV files, summary files, stitched images - Persistence: `RegionEntity` ## Configuration diff --git a/_docs/02_tasks/todo/AZ-370_refactor_status_pointtype_enums.md b/_docs/02_tasks/done/AZ-370_refactor_status_pointtype_enums.md similarity index 100% rename from _docs/02_tasks/todo/AZ-370_refactor_status_pointtype_enums.md rename to _docs/02_tasks/done/AZ-370_refactor_status_pointtype_enums.md diff --git a/_docs/_autodev_state.md b/_docs/_autodev_state.md index 79f7706..243a18f 100644 --- a/_docs/_autodev_state.md +++ b/_docs/_autodev_state.md @@ -8,7 +8,7 @@ status: in_progress sub_step: phase: 4 name: execution - detail: "K=3 review (16-18) PASS; next batch 19 candidate AZ-370" + detail: "batch 19 (AZ-370) complete; K=3 review fires after batch 21" retry_count: 0 cycle: 1 tracker: jira