[AZ-373] Refactor C20: drop MapsVersion from new writes (option a)

- Stop writing "downloaded_YYYY-MM-DD" into tiles.maps_version: new rows
  bind @MapsVersion to NULL via TileService.BuildTileEntity.
- Retain the tiles.maps_version column (coderule.mdc forbids unprompted
  column drops); pre-existing rows keep their values for forensics.
- Remove MapsVersion property from DownloadTileResponse (API wire shape)
  and TileMetadata (internal DTO); OpenAPI schema regenerates from the
  DTO via Swashbuckle.
- Add 3 AC tests in TileServiceTests covering the captured-entity write
  (AC-1) and the DTO/wire-shape removal (AC-2).
- Update integration-test local DTO + console output; refresh docs in
  common_dtos.md, services_tile_service.md, data_model.md.
- Archive AZ-373 task file: todo/ -> done/.

174 unit + 5 smoke pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 04:05:40 +03:00
parent 45f7852fb2
commit 7c37636fdf
11 changed files with 55 additions and 14 deletions
-1
View File
@@ -179,7 +179,6 @@ async Task<IResult> GetTileByLatLon([FromQuery] double Latitude, [FromQuery] dou
TileSizeMeters = tile.TileSizeMeters, TileSizeMeters = tile.TileSizeMeters,
TileSizePixels = tile.TileSizePixels, TileSizePixels = tile.TileSizePixels,
ImageType = tile.ImageType, ImageType = tile.ImageType,
MapsVersion = tile.MapsVersion,
Version = tile.Version, Version = tile.Version,
FilePath = tile.FilePath, FilePath = tile.FilePath,
CreatedAt = tile.CreatedAt, CreatedAt = tile.CreatedAt,
@@ -9,7 +9,6 @@ public record DownloadTileResponse
public double TileSizeMeters { get; set; } public double TileSizeMeters { get; set; }
public int TileSizePixels { get; set; } public int TileSizePixels { get; set; }
public string ImageType { get; set; } = string.Empty; public string ImageType { get; set; } = string.Empty;
public string? MapsVersion { get; set; }
public int? Version { get; set; } public int? Version { get; set; }
public string FilePath { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
@@ -11,7 +11,6 @@ public class TileMetadata
public double TileSizeMeters { get; set; } public double TileSizeMeters { get; set; }
public int TileSizePixels { get; set; } public int TileSizePixels { get; set; }
public string ImageType { get; set; } = string.Empty; public string ImageType { get; set; } = string.Empty;
public string? MapsVersion { get; set; }
public int? Version { get; set; } public int? Version { get; set; }
public string FilePath { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
@@ -9,7 +9,6 @@ public record DownloadTileResponse
public double TileSizeMeters { get; set; } public double TileSizeMeters { get; set; }
public int TileSizePixels { get; set; } public int TileSizePixels { get; set; }
public string ImageType { get; set; } = string.Empty; public string ImageType { get; set; } = string.Empty;
public string? MapsVersion { get; set; }
public string FilePath { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; } public DateTime UpdatedAt { get; set; }
@@ -45,7 +45,6 @@ public static class TileTests
Console.WriteLine($" Tile Size (meters): {tile.TileSizeMeters:F2}"); Console.WriteLine($" Tile Size (meters): {tile.TileSizeMeters:F2}");
Console.WriteLine($" Tile Size (pixels): {tile.TileSizePixels}"); Console.WriteLine($" Tile Size (pixels): {tile.TileSizePixels}");
Console.WriteLine($" Image Type: {tile.ImageType}"); Console.WriteLine($" Image Type: {tile.ImageType}");
Console.WriteLine($" Maps Version: {tile.MapsVersion}");
Console.WriteLine($" File Path: {tile.FilePath}"); Console.WriteLine($" File Path: {tile.FilePath}");
Console.WriteLine($" Created At: {tile.CreatedAt:yyyy-MM-dd HH:mm:ss}"); Console.WriteLine($" Created At: {tile.CreatedAt:yyyy-MM-dd HH:mm:ss}");
@@ -154,7 +154,7 @@ public class TileService : ITileService
TileSizeMeters = downloaded.TileSizeMeters, TileSizeMeters = downloaded.TileSizeMeters,
TileSizePixels = _mapConfig.TileSizePixels, TileSizePixels = _mapConfig.TileSizePixels,
ImageType = "jpg", ImageType = "jpg",
MapsVersion = $"downloaded_{now:yyyy-MM-dd}", MapsVersion = null,
Version = null, Version = null,
FilePath = downloaded.FilePath, FilePath = downloaded.FilePath,
CreatedAt = now, CreatedAt = now,
@@ -175,7 +175,6 @@ public class TileService : ITileService
TileSizeMeters = entity.TileSizeMeters, TileSizeMeters = entity.TileSizeMeters,
TileSizePixels = entity.TileSizePixels, TileSizePixels = entity.TileSizePixels,
ImageType = entity.ImageType, ImageType = entity.ImageType,
MapsVersion = entity.MapsVersion,
Version = entity.Version, Version = entity.Version,
FilePath = entity.FilePath, FilePath = entity.FilePath,
CreatedAt = entity.CreatedAt, CreatedAt = entity.CreatedAt,
@@ -207,6 +207,54 @@ public class TileServiceTests
captured!.Version.Should().BeNull("AZ-357: new code never writes the deprecated year-based version"); captured!.Version.Should().BeNull("AZ-357: new code never writes the deprecated year-based version");
} }
[Fact]
public void BuildTileEntity_DoesNotPopulateMapsVersion_AZ373_AC1()
{
// Arrange
var downloader = new Mock<ISatelliteDownloader>();
var tileRepo = new Mock<ITileRepository>();
TileEntity? captured = null;
tileRepo
.Setup(r => r.InsertAsync(It.IsAny<TileEntity>()))
.Callback<TileEntity>(e => captured = e)
.ReturnsAsync(Guid.NewGuid());
downloader
.Setup(d => d.DownloadSingleTileAsync(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new DownloadedTileInfoV2(1, 2, 18, 47.46, 37.65, "tiles/18/1/2.jpg", 100.0));
var service = BuildService(downloader, tileRepo);
// Act
_ = service.DownloadAndStoreSingleTileAsync(47.46, 37.65, 18).GetAwaiter().GetResult();
// Assert
captured.Should().NotBeNull();
captured!.MapsVersion.Should().BeNull(
"AZ-373 option (a): new code never writes a MapsVersion value; the column is retained for forensics on pre-existing rows only");
}
[Fact]
public void DownloadTileResponse_DoesNotExposeMapsVersion_AZ373_AC2()
{
// Act
var property = typeof(DownloadTileResponse).GetProperty("MapsVersion");
// Assert
property.Should().BeNull(
"AZ-373 AC-2 option (a): MapsVersion is removed from the HTTP response shape");
}
[Fact]
public void TileMetadata_DoesNotExposeMapsVersion_AZ373_AC1()
{
// Act
var property = typeof(TileMetadata).GetProperty("MapsVersion");
// Assert
property.Should().BeNull(
"AZ-373 AC-1: TileMetadata DTO no longer carries MapsVersion (consistent with the HTTP response removal)");
}
[Fact] [Fact]
public async Task GetTileAsync_KnownId_ReturnsMappedMetadata() public async Task GetTileAsync_KnownId_ReturnsMappedMetadata()
{ {
+1 -1
View File
@@ -96,7 +96,7 @@ Stores metadata for downloaded satellite imagery tiles. Each tile is a single im
| tile_size_meters | DOUBLE PRECISION | NOT NULL | Ground coverage in meters | | tile_size_meters | DOUBLE PRECISION | NOT NULL | Ground coverage in meters |
| tile_size_pixels | INT | NOT NULL | Image dimension in pixels | | tile_size_pixels | INT | NOT NULL | Image dimension in pixels |
| image_type | VARCHAR(10) | NOT NULL | Image format (e.g., "jpg") | | image_type | VARCHAR(10) | NOT NULL | Image format (e.g., "jpg") |
| maps_version | VARCHAR(50) | | Google Maps version string | | maps_version | VARCHAR(50) | | Legacy free-form provider tag; post-AZ-373 new rows write NULL (column retained for forensics on pre-existing rows) |
| version | INT | NOT NULL, DEFAULT 2025 | Year-based versioning for cache invalidation | | version | INT | NOT NULL, DEFAULT 2025 | Year-based versioning for cache invalidation |
| file_path | VARCHAR(500) | NOT NULL | Relative path to stored image | | file_path | VARCHAR(500) | NOT NULL | Relative path to stored image |
| tile_x | INT | NOT NULL | Tile X coordinate (Slippy Map) | | tile_x | INT | NOT NULL | Tile X coordinate (Slippy Map) |
+1 -1
View File
@@ -30,7 +30,7 @@ Represents a single map tile with its spatial bounds.
Metadata about a stored tile (mirrors `TileEntity` but without DB-specific concerns). Metadata about a stored tile (mirrors `TileEntity` but without DB-specific concerns).
- `Id` (Guid), `TileZoom`, `TileX`, `TileY` (int), `Latitude`, `Longitude` (double) - `Id` (Guid), `TileZoom`, `TileX`, `TileY` (int), `Latitude`, `Longitude` (double)
- `TileSizeMeters` (double), `TileSizePixels` (int), `ImageType` (string) - `TileSizeMeters` (double), `TileSizePixels` (int), `ImageType` (string)
- `MapsVersion` (string?), `Version` (int), `FilePath` (string) - `Version` (int?), `FilePath` (string)
- `CreatedAt`, `UpdatedAt` (DateTime) - `CreatedAt`, `UpdatedAt` (DateTime)
### RegionRequest ### RegionRequest
@@ -9,7 +9,7 @@ Orchestrates tile downloading and persistence. Bridges the downloader (Google Ma
### TileService (implements ITileService) ### TileService (implements ITileService)
- `DownloadAndStoreTilesAsync(double lat, double lon, double sizeMeters, int zoomLevel, CancellationToken) → Task<List<TileMetadata>>`: - `DownloadAndStoreTilesAsync(double lat, double lon, double sizeMeters, int zoomLevel, CancellationToken) → Task<List<TileMetadata>>`:
1. Queries existing tiles in the region from the repository (filtered to current year's version) 1. Queries existing tiles in the region from the repository (latest per `(latitude, longitude, zoom_level, tile_size_meters)` post-AZ-357)
2. Calls `ISatelliteDownloader.GetTilesWithMetadataAsync` with existing tiles to skip 2. Calls `ISatelliteDownloader.GetTilesWithMetadataAsync` with existing tiles to skip
3. Creates `TileEntity` for each newly downloaded tile and inserts via repository (upsert) 3. Creates `TileEntity` for each newly downloaded tile and inserts via repository (upsert)
4. Returns combined list of existing + new tile metadata 4. Returns combined list of existing + new tile metadata
@@ -19,10 +19,9 @@ Orchestrates tile downloading and persistence. Bridges the downloader (Google Ma
- `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata>` (AZ-311): download one tile by lat/lon, persist, return metadata - `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata>` (AZ-311): download one tile by lat/lon, persist, return metadata
## Internal Logic ## Internal Logic
- Version is `DateTime.UtcNow.Year` — tiles are considered fresh for the current calendar year - New rows write `Version = null` and `MapsVersion = null` (post-AZ-357 / AZ-373); the `version` and `maps_version` columns are retained for backward compatibility with pre-existing rows
- `MapToMetadata(TileEntity) → TileMetadata`: entity-to-DTO mapping (static helper) - `MapToMetadata(TileEntity) → TileMetadata`: entity-to-DTO mapping (static helper); `MapsVersion` is no longer projected onto `TileMetadata` / `DownloadTileResponse`
- Tile size hardcoded to 256 pixels, image type "jpg" - `TileSizePixels` sourced from `MapConfig.TileSizePixels` (default 256, post-AZ-371); image type fixed at `"jpg"`
- `MapsVersion` formatted as `"downloaded_{date}"`
- `IMemoryCache` keyed by `(z, x, y)` with 1h absolute / 30min sliding expiration; populated on first hit and on downloader fallback - `IMemoryCache` keyed by `(z, x, y)` with 1h absolute / 30min sliding expiration; populated on first hit and on downloader fallback
## Dependencies ## Dependencies