Files
satellite-provider/_docs/02_document/modules/common_dtos.md
T
Oleksandr Bezdieniezhnykh bc04ba7f99 [AZ-794] [AZ-795] [AZ-796] Cycle 7 Steps 12-15 sync (test-spec / docs / security / perf)
Step 12 (Test-Spec Sync): adds BT-27 for the AZ-796 9-rule
validation surface and 12 cycle-7 AC rows + Coverage Summary
update to traceability-matrix.md.

Step 13 (Update Docs): module-layout + module docs for the new
SatelliteProvider.Api/Validators namespace + GlobalExceptionHandler
+ updated TileInventory DTO; tests_unit + tests_integration
document the new InventoryRequestValidatorTests (16 unit tests
covering all 9 rules) + TileInventoryValidationTests (16
integration tests) + ProblemDetailsAssertions support;
glossary entries for Validation Problem Details / FluentValidation
/ Unmapped Member Handling; system-flows F8 (Tile Inventory Bulk
Lookup) expanded with deserializer + validator gates and a 13-row
Validation Surface table; data_parameters § Tile Inventory
documents the v2 input schema + constraints; ripple_log_cycle7
captures the doc-side ripple decisions.

Step 14 (Security Audit): 5-phase audit ran; verdict
PASS_WITH_WARNINGS (3 Low findings — D-AZ795-1 FluentValidation
12.0.0 -> 12.1.1 recommended bump, F-AZ795-1 JsonException.Message
leak in 400 detail, F-AZ795-2 BadHttpRequestException.Message leak).
No Critical / High; auth runs before validation (confirmed in
Program.cs); two NuGet additions (FluentValidation 12.0.0 +
.DependencyInjectionExtensions 12.0.0) both CVE-clean. Per-phase
reports plus consolidated security_report_cycle7.md.

Step 15 (Performance Test): docker compose stack used for perf
run, scripts/run-performance-tests.sh exited 0 with 8/8 scenarios
PASS (second consecutive clean exit-0); added PT-09 cycle-7 smoke
probe (v2 z/x/y schema, 2500-tile all-miss batch) measuring
min=27ms median=44ms p95=73ms max=86ms (13.7x under AZ-505 AC-4
1000ms budget). PT-07/08 improvements traced to the cycle-6 TLS
handshake-overhead identification, not application-side change.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 11:24:27 +03:00

9.9 KiB
Raw Blame History

Module: Common/DTO

Purpose

Data transfer objects used across all layers — API requests/responses, inter-service communication, and queue messages.

Public Interface

GeoPoint

Geographic coordinate with tolerance-based equality.

  • Lat (double): latitude, JSON property "lat"
  • Lon (double): longitude, JSON property "lon"
  • Constructor: GeoPoint(), GeoPoint(double lat, double lon)
  • Equality: two points are equal if both coordinates differ by less than 0.00005 (PRECISION_TOLERANCE)
  • Operator overloads: ==, !=

Direction

Result of a directional calculation between two points.

  • Distance (double): distance in meters
  • Azimuth (double): bearing in degrees (0360)

SatTile

Represents a single map tile with its spatial bounds.

  • X, Y (int): tile coordinates in the slippy map scheme
  • Zoom (int): zoom level
  • LeftTop, BottomRight (GeoPoint): computed bounding box corners (via GeoUtils.TileToWorldPos)
  • Url (string): download URL
  • FileName → string: formatted as {X}.{Y}.{Zoom}.jpg

TileMetadata

Metadata about a stored tile (mirrors TileEntity but without DB-specific concerns).

  • Id (Guid), TileZoom, TileX, TileY (int), Latitude, Longitude (double)
  • TileSizeMeters (double), TileSizePixels (int), ImageType (string)
  • Version (int?), FilePath (string)
  • CreatedAt, UpdatedAt (DateTime)

RegionRequest

Queue message for async region processing.

  • Id (Guid), Latitude, Longitude (double), SizeMeters (double)
  • ZoomLevel (int), StitchTiles (bool)

RegionStatus

Response DTO for region status queries.

  • Id (Guid), Status (string), CsvFilePath, SummaryFilePath (string?)
  • TilesDownloaded, TilesReused (int), CreatedAt, UpdatedAt (DateTime)

RoutePoint

Input point in a route creation request.

  • Latitude (double, JSON: "lat"), Longitude (double, JSON: "lon")

RoutePointDto

Output point in a route response (includes computed fields).

  • Latitude, Longitude (double), PointType (string: "start"/"end"/"action"/"intermediate")
  • SequenceNumber, SegmentIndex (int), DistanceFromPrevious (double?)

CreateRouteRequest

API request body for route creation.

  • Id (Guid), Name (string), Description (string?)
  • RegionSizeMeters (double), ZoomLevel (int)
  • Points (List<RoutePoint>), Geofences (Geofences?)
  • RequestMaps (bool), CreateTilesZip (bool)

RouteResponse

API response for route queries.

  • All fields from the route entity plus Points (List<RoutePointDto>)
  • MapsReady (bool), TilesZipPath (string?)

GeofencePolygon

Axis-aligned bounding box defined by NW and SE corners.

  • NorthWest (GeoPoint?), SouthEast (GeoPoint?)

Geofences

Container for multiple geofence polygons.

  • Polygons (List<GeofencePolygon>)

UavTileMetadata (added AZ-488, extended AZ-503)

Per-tile metadata payload inside a UAV batch upload (POST /api/satellite/upload). Indexed-correlated with the multipart IFormFileCollection.

  • Latitude, Longitude (double)
  • TileZoom (int)
  • TileSizeMeters (double)
  • CapturedAt (DateTime, UTC; subject to AZ-488 Rule 4 future-skew / age checks)
  • FlightId (Guid?, JSON: "flightId") — AZ-503 optional flight identifier. When set, the per-item tiles.id becomes Uuidv5(TileNamespace, "{z}/{x}/{y}/uav/{flightId}"), the on-disk path is ./tiles/uav/{flightId}/{z}/{x}/{y}.jpg, and the UPSERT conflict key separates this row from rows belonging to other flights at the same cell. When null, the per-item id uses the zero-UUID 00000000-0000-0000-0000-000000000000 placeholder and the on-disk path uses the literal none segment (./tiles/uav/none/{z}/{x}/{y}.jpg). The placeholder UUID is purely a key-space marker — it never lands in the flight_id column (which stays NULL); the UPSERT uses COALESCE(flight_id, '00000000-...') for the conflict check.

UavTileBatchMetadataPayload (added AZ-488)

JSON envelope deserialized from the metadata form field of a UAV batch upload.

  • Items (IReadOnlyList<UavTileMetadata>)

UavTileBatchUploadResponse (added AZ-488)

Wire response for POST /api/satellite/upload. Returned with HTTP 200 regardless of per-item outcomes; envelope-level failures (auth, oversize, deserialization) bypass this shape.

  • Items (List<UavTileUploadResultItem>)

UavTileUploadResultItem (added AZ-488)

Per-item result inside UavTileBatchUploadResponse.

  • Index (int): zero-based index into the request batch.
  • Status (string): one of UavTileUploadStatus.Accepted / UavTileUploadStatus.Rejected.
  • TileId (Guid?): set on accept (matches the new/updated tiles.id); null on reject.
  • RejectReason (string?): closed-enum reason code from UavTileRejectReasons; null on accept.
  • RejectDetails (string?): short human-readable note. MUST NOT leak server-internal paths / exception types / hostnames (AZ-488 Security NFR; covered by SEC-11).

UavTileUploadStatus (added AZ-488, static string constants)

  • Accepted = "accepted"
  • Rejected = "rejected"

UavTileRejectReasons (added AZ-488, static string constants — closed enumeration v1.0.0)

Authoritative reject-reason codes for the UAV upload quality gate. Adding a new code requires a minor-version bump of _docs/02_document/contracts/api/uav-tile-upload.md.

  • InvalidFormat = "INVALID_FORMAT" — Rule 1 (content-type or JPEG magic bytes).
  • SizeOutOfBand = "SIZE_OUT_OF_BAND" — Rule 2 (bytes outside [MinBytes, MaxBytes]).
  • WrongDimensions = "WRONG_DIMENSIONS" — Rule 3 (image width/height ≠ MapConfig.TileSizePixels).
  • CapturedAtFuture = "CAPTURED_AT_FUTURE" — Rule 4 (timestamp ahead of now + CapturedAtFutureSkewSeconds).
  • CapturedAtTooOld = "CAPTURED_AT_TOO_OLD" — Rule 4 (timestamp older than MaxAgeDays).
  • ImageTooUniform = "IMAGE_TOO_UNIFORM" — Rule 5 (luminance variance below MinLuminanceVariance).
  • StorageFailure = "STORAGE_FAILURE" — reserved for the orphan-row-recovery path when the on-disk write succeeds but the DB UPSERT fails; surfaced per-item without failing the envelope (AZ-488 Reliability NFR).

TileCoord (added AZ-505, renamed AZ-794 cycle 7)

Single tile coordinate triple used by the inventory endpoint Form A request shape and as the per-entry input echo on the response.

  • Z (int) [JsonRequired] — slippy zoom level. Wire name "z".
  • X (int) [JsonRequired] — slippy x at that zoom. Wire name "x".
  • Y (int) [JsonRequired] — slippy y at that zoom. Wire name "y".
  • Defined in SatelliteProvider.Common/DTO/TileInventory.cs. Matches tile-inventory.md v2.0.0 Shape (the rename from tileZoom/tileX/tileY shipped in AZ-794; the [JsonRequired] markers + the global UnmappedMemberHandling.Disallow mean missing axes and the legacy field names both surface as HTTP 400 with ValidationProblemDetails per error-shape.md v1.0.0).

TileInventoryRequest (added AZ-505)

API request body for POST /api/satellite/tiles/inventory. Carries one of two XOR-exclusive batch shapes.

  • Tiles (IReadOnlyList<TileCoord>?) — Form A: coords-by-value. The server computes location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}") per entry.
  • LocationHashes (IReadOnlyList<Guid>?) — Form B: hashes-by-reference. Used when the caller already has UUIDv5 location hashes (typical for the onboard cross-repo path).
  • Exactly one of Tiles / LocationHashes must be populated and non-empty; both-populated or neither → HTTP 400 (tile-inventory.md v2.0.0 Inv-1, enforced by InventoryRequestValidator via ValidationEndpointFilter<TileInventoryRequest> in cycle 7).
  • Total entries (in either field) ≤ TileInventoryLimits.MaxEntriesPerRequest (5000); over-cap → HTTP 400 (Inv-7).

TileInventoryEntry (added AZ-505, coord fields renamed AZ-794 cycle 7)

Per-entry result inside TileInventoryResponse. One entry per request entry, in the SAME order as the request (tile-inventory.md Inv-2).

  • Z, X, Y (int) — echoed coord triple matching the request entry; wire names "z", "x", "y" (renamed from "tileZoom"/"tileX"/"tileY" by AZ-794). Always populated; when Form B was used, these are 0 (the caller already knows the hash).
  • LocationHash (Guid) — always populated; UUIDv5 of "{z}/{x}/{y}" from Uuidv5.LocationHashForTile (Form A) or echoed from request (Form B).
  • Present (bool) — true iff a row exists in tiles with this location_hash (Inv-4).
  • Id (Guid?) — tiles.id of the most-recent row across sources/flights (captured_at DESC, updated_at DESC, id DESC, Inv-5); null when Present=false (Inv-6).
  • CapturedAt (DateTime?), Source (string?), FlightId (Guid?), ResolutionMPerPx (double?) — populated on the most-recent row; all null when Present=false.

TileInventoryResponse (added AZ-505)

API response body for POST /api/satellite/tiles/inventory.

  • Results (IReadOnlyList<TileInventoryEntry>) — one entry per request entry; Results.Count always equals the request entry count (Inv-2).

TileInventoryLimits (added AZ-505, static constants)

  • MaxEntriesPerRequest = 5000 — request-body cap enforced by InventoryRequestValidator (per-array cap; tile-inventory.md v2.0.0 Inv-7).

Internal Logic

  • GeoPoint uses a precision tolerance of 0.00005 degrees (~5.5 meters) for equality comparison.
  • SatTile eagerly computes its bounding box corners on construction by calling GeoUtils.TileToWorldPos.

Dependencies

  • GeoPoint, Direction — no imports
  • SatTileSatelliteProvider.Common.Utils.GeoUtils
  • All others — no internal dependencies (or only System.Text.Json.Serialization)

Consumers

  • All services, repositories, and API endpoints consume these DTOs
  • RegionRequest is the message type for IRegionRequestQueue

Data Models

These ARE the data models (DTOs). They map closely to the database entities but are decoupled from the persistence layer.

Configuration

None consumed directly.

External Integrations

None.

Security

None.

Tests

No dedicated DTO tests.