Files
satellite-provider/_docs/02_document/system-flows.md
T
Oleksandr Bezdieniezhnykh 5e056b2334 [AZ-809] Strict validation for POST /api/satellite/route
Third concrete child of AZ-795 (cycle 8 batch 3). FluentValidation +
[JsonRequired] + UnmappedMemberHandling.Disallow combine to reject every
malformed payload at the API boundary with RFC 7807 ValidationProblemDetails.

Validators (SatelliteProvider.Api/Validators/, all new)
- CreateRouteRequestValidator: id non-empty, name/description length,
  regionSizeMeters/zoomLevel ranges, points count [2, 500], cross-field
  createTilesZip => requestMaps. Chains RoutePointValidator (per-point)
  and GeofencePolygonValidator (per-polygon, guarded by When(Geofences != null)).
  OverridePropertyName("geofences.polygons") on the geofences chain so
  FluentValidation's default leaf-only key policy doesn't drop the parent
  path on deep expressions like req.Geofences!.Polygons.
- RoutePointValidator: lat/lon ranges; OverridePropertyName("lat"/"lon")
  chained AFTER InclusiveBetween (the extension is defined on
  IRuleBuilderOptions<T, TProperty>, so the generic type is only
  inferable after the first concrete rule) so error keys match the
  wire format (`points[i].lat`) rather than the C# property name
  (`points[i].latitude`).
- GeofencePolygonValidator: per-corner range checks via private nested
  GeoCornerValidator; cross-field NW.Lat > SE.Lat and NW.Lon < SE.Lon
  invariants emit at errors["geofences.polygons[i].northWest"].

DTOs (SatelliteProvider.Common/DTO/, [JsonRequired] additions only)
- CreateRouteRequest: id, name, regionSizeMeters, zoomLevel, points,
  requestMaps, createTilesZip
- RoutePoint: Latitude, Longitude
- GeofencePolygon: NorthWest, SouthEast; Geofences: Polygons
- GeoPoint: Lat, Lon

Tests
- Unit: 26 methods total — 16 in CreateRouteRequestValidatorTests, 6 in
  GeofencePolygonValidatorTests, 4 in RoutePointValidatorTests. Each
  RuleFor/RuleForEach chain has at least one positive + one negative case.
- Integration: CreateRouteValidationTests.cs — 16 methods (happy + 15
  failure modes) wired into smoke + full suites. Covers empty body,
  missing/zero id, empty name, out-of-range regionSizeMeters/zoomLevel,
  points count < 2, per-point lat/lon out-of-range, geofence invariants,
  missing requestMaps, cross-field createTilesZip, unknown root field,
  nested type mismatch.
- Manual probe: scripts/probe_route_validation.sh curl-exercises every
  failure mode end-to-end + happy path.

Docs
- New contract _docs/02_document/contracts/api/route-creation.md v1.0.0
  with nested DTO chain, invariants, per-field test cases table, and
  advisories on the legacy service-layer RouteValidator + the
  input/output RoutePoint vs RoutePointDto naming asymmetry.
- system-flows.md F4 sequence diagram extended with the validation-filter
  branch; preconditions + error scenarios reference the new contract.
- modules/api_program.md: CreateRoute handler section added; Api/Validators
  bumped to AZ-808/AZ-809/AZ-811.
- modules/common_dtos.md: DTO descriptions updated with [JsonRequired]
  annotations and constraint summaries.
- tests/blackbox-tests.md BT-06/BT-N03/BT-N04/BT-N05 align with the new
  wire format and named error keys.
- tests/security-tests.md SEC-04 references GlobalExceptionHandler's
  JsonException branch + AZ-353 correlationId.
- _docs/03_implementation/batch_03_cycle8_report.md + reviews/batch_03_cycle8_review.md
  (PASS_WITH_NOTES — F1 Low: OverridePropertyName documented inline,
  F2 + F3 Info: pre-existing advisories for follow-up).

Smoke green (mode=smoke, exit 0). AZ-809 transitioned to In Testing on Jira.
Task file moved to _docs/02_tasks/done/.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 17:49:48 +03:00

20 KiB

Satellite Provider — System Flows

Flow Inventory

# Flow Name Trigger Primary Components Criticality
F1 Single Tile Download HTTP GET /api/satellite/tiles/latlon WebApi, TileDownloader, DataAccess High
F2 Region Request HTTP POST /api/satellite/request WebApi, RegionProcessing, TileDownloader, DataAccess High
F3 Region Processing Queue dequeue (background) RegionProcessing, TileDownloader, DataAccess High
F4 Route Creation HTTP POST /api/satellite/route WebApi, RouteManagement, DataAccess High
F5 Route Map Processing Queue dequeue (background) RouteManagement, RegionProcessing, TileDownloader, DataAccess Medium
F6 Status Query HTTP GET /api/satellite/region/{id} or /route/{id} WebApi, DataAccess Low
F7 Leaflet Tile Serving HTTP GET /tiles/{z}/{x}/{y} WebApi, TileService, DataAccess, FileSystem High
F8 Tile Inventory Bulk Lookup HTTP POST /api/satellite/tiles/inventory WebApi, TileService, DataAccess High

Flow Dependencies

Flow Depends On Shares Data With
F1 F3 (tile cache)
F2 F3 (triggers it)
F3 F2 enqueues work F1 (shares tile cache), F5
F4 F5 (triggers it)
F5 F4 must create route first F3 (submits region requests)
F6 F2/F4 must exist
F7 F1 or F3 must have populated the tile (else 404) F1, F3, F8 (shares tiles.location_hash)
F8 F1, F3, F7 (shares tiles.location_hash)

Flow F1: Single Tile Download

Description

Client requests a single satellite tile by geographic coordinates and zoom level. The service checks the cache (DB), downloads from Google Maps if not cached, stores it, and returns metadata. The wire-format contract is _docs/02_document/contracts/api/tile-latlon.md v1.0.0; failure responses follow error-shape.md v1.0.0.

Preconditions

  • Query params lat ∈ [-90, 90], lon ∈ [-180, 180], zoom ∈ [0, 22]. Any unknown query key (e.g. legacy ?Latitude= typo) is rejected by RejectUnknownQueryParamsEndpointFilter (AZ-811 cycle 8) with HTTP 400. Range checks via GetTileByLatLonQueryValidator.
  • Google Maps session token configured

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebApi
    participant TileService
    participant TileRepo
    participant GoogleMaps
    participant FileSystem

    Client->>WebApi: GET /api/satellite/tiles/latlon?lat&lon&zoom
    WebApi->>TileService: DownloadTileAsync(lat, lon, zoom)
    TileService->>TileRepo: FindByCoordinates(lat, lon, zoom)
    alt Tile exists in cache
        TileRepo-->>TileService: TileEntity
        TileService-->>WebApi: TileMetadata (cached)
    else Not cached
        TileService->>GoogleMaps: Download tile image
        GoogleMaps-->>TileService: JPEG bytes
        TileService->>FileSystem: Save to ./tiles/{zoom}/{x}/{y}.jpg
        TileService->>TileRepo: Insert(TileEntity)
        TileService-->>WebApi: TileMetadata (new)
    end
    WebApi-->>Client: JSON response

Error Scenarios

Error Where Detection Recovery
Google Maps timeout Download step HttpClient timeout Return error to caller
Duplicate download race Concurrent requests ConcurrentDictionary check Await existing download
Disk full File save IOException Exception propagated, region fails

Flow F2: Region Request

Description

Client submits a region definition (center point, size, zoom). The request is persisted and queued for asynchronous processing. The wire-format contract is _docs/02_document/contracts/api/region-request.md v1.0.0; failure responses follow error-shape.md v1.0.0.

Preconditions

  • Valid region parameters: non-zero id (UUID), lat ∈ [-90, 90], lon ∈ [-180, 180], sizeMeters ∈ [100, 10000], zoomLevel ∈ [0, 22], explicit stitchTiles (bool, no default). Enforced by RegionRequestValidator + [JsonRequired] at the API edge (AZ-808 cycle 8).

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebApi
    participant RegionService
    participant RegionRepo
    participant Queue

    Client->>WebApi: POST /api/satellite/request {lat, lon, size, zoom}
    WebApi->>RegionService: CreateRegionRequest(dto)
    RegionService->>RegionRepo: Insert(RegionEntity status=pending)
    RegionRepo-->>RegionService: region_id
    RegionService->>Queue: Enqueue(region_id)
    RegionService-->>WebApi: region_id
    WebApi-->>Client: 200 OK {region_id}

Error Scenarios

Error Where Detection Recovery
Queue full Enqueue step Channel at capacity Return 503 / reject request
DB insert failure Persist step Exception Return 500

Flow F3: Region Processing (Background)

Description

Background service dequeues region IDs, calculates tile grid, downloads all tiles (with concurrency control), optionally stitches them, and produces output files (CSV, summary, stitched image).

Preconditions

  • Region exists in DB with status "pending"
  • Google Maps session token configured

Sequence Diagram

sequenceDiagram
    participant Queue
    participant RegionProcessor
    participant RegionService
    participant TileService
    participant GoogleMaps
    participant RegionRepo
    participant FileSystem

    Queue->>RegionProcessor: Dequeue region_id
    RegionProcessor->>RegionRepo: GetById(region_id)
    RegionProcessor->>RegionRepo: UpdateStatus(processing)
    loop For each tile in grid
        RegionProcessor->>TileService: DownloadTileAsync(lat, lon, zoom)
        TileService->>GoogleMaps: Download (if not cached)
    end
    RegionProcessor->>FileSystem: Write CSV (tile manifest)
    RegionProcessor->>FileSystem: Write summary file
    opt stitch_tiles = true
        RegionProcessor->>FileSystem: Stitch tiles into composite image
    end
    RegionProcessor->>RegionRepo: UpdateStatus(completed, file paths)

Data Flow

Step From To Data Format
1 Queue RegionProcessor region_id int
2 RegionProcessor TileService lat, lon, zoom per tile method call
3 TileService FileSystem JPEG image file
4 RegionProcessor FileSystem tile manifest CSV
5 RegionProcessor FileSystem region summary TXT
6 RegionProcessor FileSystem composite image JPEG

Error Scenarios

Error Where Detection Recovery
Tile download failure Per-tile loop Exception from TileService Log, continue with remaining tiles
All tiles fail After loop Zero tiles downloaded Mark region as "failed"
Stitch failure Image processing ImageSharp exception Mark region failed, tiles still available

Flow F4: Route Creation

Description

Client submits a route (ordered waypoints + optional geofence polygons). The service interpolates intermediate points every ~200m and persists the full point set. The wire-format contract is _docs/02_document/contracts/api/route-creation.md v1.0.0; failure responses follow error-shape.md v1.0.0.

Preconditions

  • JWT in Authorization: Bearer <token> validates against the API's signing key, issuer, and audience (.RequireAuthorization()).
  • Request body deserializes successfully: all [JsonRequired] axes present (id, name, regionSizeMeters, zoomLevel, points, requestMaps, createTilesZip, plus per-point lat/lon, per-polygon northWest/southEast, per-corner lat/lon, geofences.polygons when geofences present); no unknown root or nested fields (UnmappedMemberHandling.Disallow).
  • CreateRouteRequestValidator rules pass: non-zero id, name length [1, 200], description length ≤ 1000, regionSizeMeters ∈ [100, 10000], zoomLevel ∈ [0, 22], points count ∈ [2, 500] with each point's lat/lon in range, per-polygon corner ranges + NW-of-SE invariants, createTilesZip ⇒ requestMaps.

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebApi
    participant ValidationFilter
    participant RouteService
    participant RouteRepo
    participant GeoUtils

    Client->>WebApi: POST /api/satellite/route {id, name, points, geofences?, ...}
    WebApi->>ValidationFilter: .WithValidation<CreateRouteRequest>()
    alt validation fails
        ValidationFilter-->>Client: 400 ValidationProblemDetails (errors{path→msg})
    else validation passes
        WebApi->>RouteService: CreateRoute(request)
        RouteService->>GeoUtils: Interpolate points between waypoints
        GeoUtils-->>RouteService: All points (original + intermediate)
        RouteService->>RouteRepo: InsertRoute(RouteEntity)
        RouteService->>RouteRepo: InsertPoints(RoutePointEntities)
        RouteService-->>WebApi: RouteResponse
        WebApi-->>Client: 200 OK {id, totalPoints, totalDistanceMeters, ...}
    end

Error Scenarios

Error Where Detection Recovery
Missing [JsonRequired] axis / unknown field / type mismatch Deserializer JsonExceptionGlobalExceptionHandler Return 400 ValidationProblemDetails (per error-shape.md v1.0.0)
Validator rule violation (range, count, cross-field) ValidationEndpointFilter<CreateRouteRequest> CreateRouteRequestValidator + nested RoutePointValidator / GeofencePolygonValidator Return 400 with errors{path→msg} map
DB insert failure Persist step Exception Return 500 (sanitised body + correlationId per AZ-353)

Flow F5: Route Map Processing (Background)

Description

When a route requests map tiles (request_maps = true), a background service creates region requests for each route point, optionally filtered by geofence, then waits for all regions to complete and produces a ZIP archive.

Preconditions

  • Route exists with request_maps = true
  • Route points already interpolated and persisted

Sequence Diagram

sequenceDiagram
    participant RouteProcessor
    participant RouteRepo
    participant RegionService
    participant Queue
    participant RegionProcessor
    participant FileSystem

    RouteProcessor->>RouteRepo: GetRouteWithPoints(route_id)
    loop For each route point
        RouteProcessor->>RouteProcessor: Check geofence (point-in-polygon)
        opt Point inside geofence (or no geofence)
            RouteProcessor->>RegionService: CreateRegionRequest(point)
            RegionService->>Queue: Enqueue(region_id)
        end
    end
    RouteProcessor->>RouteProcessor: Wait for all regions to complete
    opt create_tiles_zip = true
        RouteProcessor->>FileSystem: Create ZIP of all tiles (max 50MB)
        RouteProcessor->>RouteRepo: Update tiles_zip_path
    end

Error Scenarios

Error Where Detection Recovery
Region processing timeout Wait loop Polling timeout Mark route partially complete
ZIP exceeds 50MB ZIP creation Size check during write Truncate or skip
Geofence calculation error Point-in-polygon Exception Include point (fail-open)

Flow F6: Status Query

Description

Client polls for the status of a region or route by ID. Returns current processing state and output file paths when complete.

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebApi
    participant DataAccess

    Client->>WebApi: GET /api/satellite/region/{id}
    WebApi->>DataAccess: GetRegionById(id)
    DataAccess-->>WebApi: RegionEntity (status, file paths)
    WebApi-->>Client: JSON {status, files}

Flow F7: Leaflet Tile Serving (added AZ-310, rewired AZ-505)

Description

Leaflet (or any HTTP/2-capable client) requests a single tile body by slippy (z, x, y). The handler resolves the most-recent variant across sources/flights by location_hash and streams the JPEG body back. AZ-505 rewired the lookup predicate from (tile_zoom, tile_x, tile_y) to location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}") so the read hits the tiles_leaflet_path covering index as an Index Only Scan; the selection rule (captured_at DESC, updated_at DESC, id DESC LIMIT 1) is unchanged from AZ-484 / AZ-503-foundation. Kestrel runs Http1AndHttp2 over TLS (https://+:8080 in dev) so ALPN multiplexes many concurrent leaflet requests on a single TLS connection.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Kestrel
    participant ServeTile
    participant TileService
    participant TileRepo
    participant FileSystem

    Client->>Kestrel: GET /tiles/{z}/{x}/{y} (HTTP/2 via TLS+ALPN)
    Kestrel->>ServeTile: route match
    ServeTile->>TileService: GetOrDownloadTileAsync(z, x, y)
    TileService->>TileRepo: GetByTileCoordinatesAsync(z, x, y)
    Note over TileRepo: WHERE location_hash = $1<br/>ORDER BY captured_at DESC, updated_at DESC, id DESC<br/>LIMIT 1<br/>Index Only Scan: tiles_leaflet_path
    alt Cached
        TileRepo-->>TileService: TileEntity
        TileService->>FileSystem: read file_path
        FileSystem-->>TileService: JPEG bytes
        TileService-->>ServeTile: TileBytes (file_path, ETag, Cache-Control)
        ServeTile-->>Client: 200 OK, JPEG body
    else Miss
        TileService->>TileService: download via GoogleMapsDownloaderV2
        Note over TileService: persists row + on-disk path, falls through to ServeTile
        TileService-->>ServeTile: TileBytes
        ServeTile-->>Client: 200 OK, JPEG body
    end

Error Handling

Failure Detection Handling
Tile not present and downloader rejects (404 from Google Maps) TileService.GetOrDownloadTileAsync propagates the downloader's HttpRequestException Returns 500; ServeTile does NOT translate this to 404 because the predicate matched (path-traversal cases below 404 earlier in routing)
Path traversal in /tiles/... segment ASP.NET Core route binding 400/404 before the handler runs (covered by SEC-02)

Flow F8: Tile Inventory Bulk Lookup (added AZ-505; renamed + strict-validated AZ-794+AZ-795+AZ-796, cycle 7)

Description

Programmatic clients (httpx http2=True, .NET HttpClient, onboard cross-repo callers) post a batch of up to 5000 {z, x, y} triples (Form A; the wire field names were renamed from tileZoom/tileX/tileY by AZ-794, cycle 7) or up to 5000 pre-computed location_hash UUIDs (Form B) and get one inventory entry per input slot, in the same order. Each entry says whether the cell is present and — when present — the most-recent row's id, capturedAt, source, flightId, and resolutionMPerPx. No tile bodies are returned; the caller subsequently fetches bodies via F7. This is the read-half of the bulk-list contract that the onboard gps-denied-onboard workspace consumes to decide which Google-Maps cells it needs and which UAV variants are already on the server.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Kestrel
    participant GetTilesInventory
    participant TileService
    participant TileRepo

    Client->>Kestrel: POST /api/satellite/tiles/inventory (JWT, Form A or B)
    Kestrel->>GetTilesInventory: route match
    Note over Kestrel,GetTilesInventory: AZ-795 deserializer guards (UnmappedMemberHandling.Disallow + [JsonRequired])<br/>catch unknown / missing / type-mismatched fields → 400 ValidationProblemDetails<br/>via GlobalExceptionHandler (cycle 7)
    GetTilesInventory->>GetTilesInventory: ValidationEndpointFilter<TileInventoryRequest><br/>(InventoryRequestValidator — AZ-796 cycle 7)
    Note over GetTilesInventory: 9 business rules: XOR / non-empty / per-array cap / Z range / X+Y range
    GetTilesInventory->>TileService: GetInventoryAsync(request)
    Note over TileService: Form A: compute location_hash per coord<br/>via Uuidv5.LocationHashForTile<br/>Form B: echo caller-supplied hashes
    TileService->>TileRepo: GetTilesByLocationHashesAsync(hashes)
    Note over TileRepo: NpgsqlCommand:<br/>SELECT DISTINCT ON (location_hash) ...<br/>WHERE location_hash = ANY($1::uuid[])<br/>ORDER BY location_hash, captured_at DESC, updated_at DESC, id DESC<br/>(bypasses Dapper IEnumerable expansion)
    TileRepo-->>TileService: IReadOnlyDictionary<Guid, TileEntity>
    TileService->>TileService: shape into TileInventoryEntry[] in request order
    TileService-->>GetTilesInventory: TileInventoryResponse
    GetTilesInventory-->>Client: 200 OK, JSON (results in input order)

Validation Surface (post-cycle 7 — AZ-795 + AZ-796)

Input Detection Response
Empty / missing body System.Text.Json ([JsonRequired] on Tiles/LocationHashes covered indirectly via InventoryRequestValidator) 400 + ValidationProblemDetails
Both tiles and locationHashes populated InventoryRequestValidator .Must(...) (Rule 1, XOR — tile-inventory.md v2.0.0 Inv-1) 400 + ValidationProblemDetails, key ""
Neither populated InventoryRequestValidator .Must(...) (Rule 1, XOR) 400 + ValidationProblemDetails, key ""
tiles or locationHashes array is empty InventoryRequestValidator .Must(...) (Rule 1, XOR — non-empty arm) 400 + ValidationProblemDetails, key ""
tiles.Count > 5000 (TileInventoryLimits.MaxEntriesPerRequest) InventoryRequestValidator .Must(t => t.Count <= 5000) (Rule 6 — Inv-7) 400 + ValidationProblemDetails, key tiles
locationHashes.Count > 5000 InventoryRequestValidator .Must(h => h.Count <= 5000) (Rule 7 — Inv-7) 400 + ValidationProblemDetails, key locationHashes
tiles[i].z missing OR out of range (must be 0..22 inclusive) [JsonRequired] on Z (deserializer) + TileCoordValidator Rule 4 400 + ValidationProblemDetails, key tiles[i].z
tiles[i].x missing OR < 0 OR >= 2^z [JsonRequired] on X (deserializer) + TileCoordValidator Rule 5 400 + ValidationProblemDetails, key tiles[i].x
tiles[i].y missing OR < 0 OR >= 2^z [JsonRequired] on Y (deserializer) + TileCoordValidator Rule 5 400 + ValidationProblemDetails, key tiles[i].y
Legacy tileZoom/tileX/tileY field names UnmappedMemberHandling.Disallow (deserializer; AZ-794 + AZ-795) 400 + ValidationProblemDetails, key tiles[i].tileZoom (etc.)
Unknown root or nested field UnmappedMemberHandling.Disallow (deserializer; AZ-795) 400 + ValidationProblemDetails, key on the unknown path
Wrong JSON type (e.g. "z": "18") System.Text.Json type-mismatch (deserializer; AZ-795) 400 + ValidationProblemDetails, key on the offending path
No Authorization: Bearer … header .RequireAuthorization() 401 before handler runs

All 4xx bodies conform to error-shape.md v1.0.0. The same ValidationProblemDetails shape is emitted whether the failure was caught by the FluentValidation business-rule layer (InventoryRequestValidator) or by the deserializer layer (via GlobalExceptionHandler). Both layers are unit + integration tested in SatelliteProvider.Tests/Validators/InventoryRequestValidatorTests and SatelliteProvider.IntegrationTests/TileInventoryValidationTests.

Performance

p95 ≤ 1000 ms for 2500-coord batches (AZ-505 AC-4). Cycle-6 measured: p95=66ms — well under budget. The covering index (tiles_leaflet_path) supplies the leading location_hash lookup; the projection's columns beyond the INCLUDE list (id, captured_at, flight_id, ...) trigger a bounded heap fetch which is documented and accepted per the AZ-505 NFR.