# Batch 12 Report — Refactor 03 Phase 3 (start) Date: 2026-05-10 Epic: AZ-350 (03-code-quality-refactoring) Status: ✅ Complete, pushed ## Scope (1 task / 2 SP) | ID | C-ID | Title | Points | Component | |----|------|-------|--------|-----------| | AZ-366 | C13 | Consolidate Haversine + tile-coord parsing into Common/Utils | 2 | Common + Services.RouteManagement | First task of Phase 3 (Structural cleanup). Single-task batch — keeps the review surface small while the previous Phase 2 changes settle. ## Changes ### Production - **MODIFIED** `SatelliteProvider.Common/Configs/StorageConfig.cs` - Added `static bool TryExtractTileCoordinates(string filePath, out int tileX, out int tileY)`. Pure parser — no I/O, no logging, no exceptions for malformed input (caller decides). Co-located with `GetTileFilePath` which is the writer side, so format changes can never drift between the two ends. This is the structural fix called for in AC-2. - The method is `static` because it's a pure string-parsing computation with no side effects, satisfying the `coderule.mdc` static-method test ("pure, self-contained computations"). - **MODIFIED** `SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs` - Deleted private `static double CalculateDistance(double lat1, double lon1, double lat2, double lon2)` (the duplicate Haversine using `EARTH_RADIUS = 6371000`). Replaced its single call site with `GeoUtils.CalculateDistance(GeoPoint, GeoPoint)` (which uses `EARTH_RADIUS = 6378137`). - `ExtractTileCoordinatesFromFilename(string)` is now a thin wrapper: it delegates parsing to `StorageConfig.TryExtractTileCoordinates` and emits the existing warning log when parsing fails. Public/internal contract preserved (callers see the same `(int TileX, int TileY)` signature, same `(-1, -1)` sentinel, same warning log behavior on malformed input). - Updated the warning message text to `tile____` (matches what `GetTileFilePath` actually writes; the old message said `tile___` which was wrong — the format is `tile_{zoom}_{x}_{y}_{ts}.jpg`). ### Tests - **NEW** `SatelliteProvider.Tests/StorageConfigTests.cs` — 6 unit tests for the new helper: - `TryExtractTileCoordinates_ValidFilename_ReturnsTrue_AZ366` — happy path with full directory path. - `TryExtractTileCoordinates_FilenameWithoutDirectory_ReturnsTrue_AZ366` — bare filename. - `TryExtractTileCoordinates_NonTilePrefix_ReturnsFalseAndSentinel_AZ366` — wrong prefix. - `TryExtractTileCoordinates_NonNumericCoords_ReturnsFalseAndSentinel_AZ366` — coords not parseable. - `TryExtractTileCoordinates_NullPath_ThrowsArgumentNullException_AZ366` — null contract. - `GetTileFilePath_RoundTrip_ParserRecoversOriginalCoordinates_AZ366_AC2` — proves writer/parser inverse pair. This is the structural assertion behind AC-2: writing then parsing recovers the original coordinates, which can only stay true if both methods live in the same module. - **PRESERVED** all 4 existing `RouteProcessingServiceTests.ExtractTileCoordinatesFromFilename_*_AC1/AC2` tests (added in batch 7 for AZ-352). They still pass against the now-thin wrapper, proving behavior preservation through the refactor. ## Verification - **Unit tests**: 77 / 77 passing (was 71 → +6 new `StorageConfigTests`). - **Integration smoke + full suite**: green. Container exits 0. The route-tile workflow (ExtractTileCoordinatesFromFilename + region-matching distance) is exercised end-to-end by every route test that requests maps. - **AC-1 grep verification**: `rg 'Math\.Sin.*Math\.PI|earthRadius|EarthRadius|EARTH_RADIUS' --type=cs` returns matches only in `SatelliteProvider.Common/Utils/GeoUtils.cs`. No Haversine implementation lives anywhere else in the codebase. ## Acceptance criteria coverage | AC | Evidence | |----|----------| | **AC-1** One Haversine implementation in the codebase | Grep for `Math.Sin.*Math.PI` and `EARTH_RADIUS` returns matches only in `GeoUtils.cs`. The local `RouteProcessingService.CalculateDistance` (which used `6371000`) has been deleted; its single call site now uses `GeoUtils.CalculateDistance`. | | **AC-2** Writer and parser co-located | Both `GetTileFilePath` and `TryExtractTileCoordinates` live as methods on `StorageConfig` in `SatelliteProvider.Common/Configs/StorageConfig.cs`. New unit test `GetTileFilePath_RoundTrip_ParserRecoversOriginalCoordinates_AZ366_AC2` proves the inverse-pair invariant. | | **AC-3** Tests stay green | 77 unit + smoke + full integration green; 4 pre-existing AZ-352 tests for `ExtractTileCoordinatesFromFilename` still pass against the thin-wrapper version. | ## Behavior preservation notes - **Earth radius constant change**: `RouteProcessingService.CalculateDistance` used `6_371_000` (mean Earth radius), `GeoUtils.CalculateDistance` uses `6_378_137` (WGS84 equatorial radius). Absolute distances differ by ~0.1%. The single call site uses the value only for `OrderBy(...).FirstOrDefault()` to pick the nearest region — both formulas are monotonic in actual distance, so the chosen region is the same for any input. No region-matching test asserts on a specific distance value. - **Public API**: `StorageConfig` adds one new static method. `GetTileFilePath` is unchanged. `RouteProcessingService.ExtractTileCoordinatesFromFilename` keeps the same signature and same observable behavior (returns `(-1, -1)` and logs a warning on malformed input). - **Warning log message text changed**: `tile___` → `tile____` (matches `GetTileFilePath`'s actual output format `tile_{z}_{x}_{y}_{ts}.jpg`). The old text was misleading. No test asserted on the message text — the existing AZ-352 tests check that the warning contains the offending filename, not the format hint. ## Up next - **Batch 13** (per the user-selected sequence): Remaining Phase 3 — natural next is AZ-368 (C15, TileCsvWriter, 2 SP). After C15 lands, C13+C15 together complete the foundational extracts that C11/C12 depend on. - **Cumulative review** next fires after batches 10+11+12 (K=3 trigger). AZ-357 + AZ-362 + AZ-366 are the three batches in the window — running the cumulative review now is the natural gate before starting batch 13.