Move 5 cross-component DTOs (GetSatelliteTilesResponse, SatelliteTile,
SaveResult, DownloadTileResponse, RequestRegionRequest) to
SatelliteProvider.Common/DTO/. Keep UploadImageRequest in the API
project under SatelliteProvider.Api.DTOs (IFormFile depends on
Microsoft.AspNetCore.Http; pulling it into Common would force an
ASP.NET framework reference into the foundation layer and break the
module-layout "Common: Imports from: (none)" invariant). Move
ParameterDescriptionFilter to SatelliteProvider.Api.Swagger.
Program.cs shrinks from 366 to 257 lines and now contains only
endpoint wiring (AC-1). JSON wire shape and Swagger schema names are
preserved (AC-2). 84 unit + full integration suite green (AC-3).
Co-authored-by: Cursor <cursoragent@cursor.com>
Both RegionService.GenerateCsvFileAsync and
RouteProcessingService.GenerateRouteCsvAsync wrote the same CSV
shape: header "latitude,longitude,file_path", same
OrderByDescending(Latitude).ThenBy(Longitude) ordering, same F6
numeric format. Two near-identical writers with no shared abstraction.
Extracted TileCsvWriter (instance class, no DI dependencies) plus a
TileCsvRow record bridging the per-pipeline DTOs (TileMetadata vs
TileInfo) to a single contract. The header constant, ordering rule,
and StreamWriter lifecycle now live in one place.
Both call sites collapse to a one-line projection plus a delegated
WriteAsync call. Region method becomes static (no longer references
instance state). Route method preserves its existing logger line.
Coverage:
- 7 new unit tests including a byte-for-byte equivalence test that
writes the same input via both the new TileCsvWriter and the
inlined-original code path side by side and asserts file bytes
are identical.
- Integration smoke + full suite green; route + region CSV outputs
unchanged across all existing scenarios (verified by extended-route
CSV verification step in the integration suite).
- 84/84 unit tests pass (was 77).
Side improvement: writer now respects CancellationToken mid-loop.
The pre-refactor inline code did not. Strict improvement; consistent
with every other async API in the codebase.
Co-authored-by: Cursor <cursoragent@cursor.com>
RouteProcessingService.CalculateDistance(double, double, double, double)
re-implemented Haversine using EARTH_RADIUS=6371000 alongside the
canonical GeoUtils.CalculateDistance(GeoPoint, GeoPoint) which uses
EARTH_RADIUS=6378137. Two implementations of the same formula for the
same problem.
Separately, ExtractTileCoordinatesFromFilename in RouteProcessingService
parsed the tile_{z}_{x}_{y}_{ts}.jpg filename pattern that's *generated*
by StorageConfig.GetTileFilePath in another assembly — the writer and
parser were coupled by string convention only and a format change in
one would silently break the other.
Both fixes:
(a) Deleted the duplicate Haversine in RouteProcessingService. The
single call site (region-matching nearest-neighbor OrderBy) now uses
GeoUtils.CalculateDistance with GeoPoint instances. The constant
difference is monotonic-equivalent for OrderBy purposes — same region
is picked.
(b) Added static StorageConfig.TryExtractTileCoordinates(string, out
int, out int): bool — pure parser, co-located with GetTileFilePath so
the inverse-pair invariant is structural, not by-convention.
RouteProcessingService.ExtractTileCoordinatesFromFilename becomes a
thin wrapper that delegates to the helper and emits the existing
warning log on malformed input — the AZ-352 tests for warning behavior
all still pass.
Verification:
- 77/77 unit tests green (was 71 → +6 new StorageConfigTests including
a writer/parser round-trip test for AC-2).
- Smoke + full integration suite green.
- AC-1 grep verification: Math.Sin/EARTH_RADIUS patterns are now
confined to GeoUtils.cs only.
Co-authored-by: Cursor <cursoragent@cursor.com>
AZ-357 — eliminate year-based tile cache expiry (LF-1):
- Migration 012: drop 5-col unique index, dedupe by (lat,lon,zoom,
size) keeping max(updated_at), add new 4-col unique index, make
version column nullable + drop default. Column itself preserved
per coderule (column drops require explicit confirmation; tracked
in AZ-373 / C20).
- TileEntity.Version, TileMetadata.Version, DownloadTileResponse.
Version: int -> int? (HTTP shape preserved; field still in JSON).
- TileService.DownloadAndStoreTilesAsync: drop currentVersion year
computation and the .Where(t => t.Version == currentVersion)
cache filter. BuildTileEntity: drop year arg; write Version=null.
- TileRepository: ON CONFLICT now 4-col; lookup queries
ORDER BY updated_at DESC instead of version DESC.
- Tests: replace inverted BT02b with positive AZ357_AC1
(prior-year cached tile is reused). Add BuildTileEntity_
DoesNotPopulateVersion_AZ357 to enforce the no-write contract.
- 69 unit + 5 smoke + 3 stub-contract integration tests pass.
Cumulative code review (batches 7-9, 7 tasks): VERDICT=PASS.
Report at _docs/03_implementation/reviews/batch_09_review.md.
Zero Critical/High/Medium/Low findings. Architecture baseline
remains clean.
Co-authored-by: Cursor <cursoragent@cursor.com>
Phase B of architecture coupling refactor (epic AZ-309). Replaces
the monolithic SatelliteProvider.Services with three per-component
csprojs to add a compiler-enforced module boundary (resolves F4):
- SatelliteProvider.Services.TileDownloader
- SatelliteProvider.Services.RegionProcessing
- SatelliteProvider.Services.RouteManagement
DI registrations relocated into per-component AddTileDownloader /
AddRegionProcessing / AddRouteManagement extension methods called
from Program.cs. RateLimitException moved to Common/Exceptions/ to
keep the three new csprojs as siblings (no Region->TileDownloader
ProjectReference). Dockerfiles and consumer csprojs (Api, Tests)
rewired to the new project paths. No DI lifetime or hosted-service
order changes.
Build: 0 warn, 0 err. Unit tests: 40/40. Smoke integration: green.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move cache+DB+download logic for /tiles/{z}/{x}/{y} and
/api/satellite/tiles/latlon out of Program.cs into TileService.
Endpoints now inject only ITileService + ILogger. Service owns
IMemoryCache (1h absolute / 30min sliding preserved). Added
TileBytes DTO; ITileService gains GetOrDownloadTileAsync and
DownloadAndStoreSingleTileAsync. 5 new unit tests cover cache
hit, repo hit, downloader fallback, and AZ-311 happy + error.
Build clean (0/0), unit suite 40/40. Resolves architecture
baseline F3 in code (docs handled by AZ-315).
Co-authored-by: Cursor <cursoragent@cursor.com>