# 02 — Mission Planning (Missions + Waypoints + Cross-Service Cascade) **Spec source**: `../../../suite/_docs/02_missions.md` § "Missions" (items 1-9), `../../../suite/_docs/00_database_schema.md` § `Missions` + `Waypoints`. **Required permission**: `FL`. **Implementation status**: ✅ implemented (with two divergences -- see Caveats). > **NOTE (forward-looking)**: file paths, route prefixes, and identifiers below reflect the post-rename state. Today's source still uses `Flight*` filenames + `[Route("flights")]` and the cascade still touches `orthophotos` + `gps_corrections`. Renames + cascade shrink tracked under Jira AZ-EPIC children B6 (rename), B7 (GPS-Denied removal), B8 (HTTP routes). **Files** (post-rename): - HTTP: `Controllers/MissionsController.cs` (parent + nested waypoint routes) - Services: `Services/MissionService.cs`, `Services/WaypointService.cs` - DTOs: `DTOs/CreateMissionRequest.cs`, `DTOs/UpdateMissionRequest.cs`, `DTOs/GetMissionsQuery.cs`, `DTOs/CreateWaypointRequest.cs`, `DTOs/UpdateWaypointRequest.cs`, `DTOs/GeoPoint.cs` - Resource enums: `Enums/WaypointSource.cs`, `Enums/WaypointObjective.cs` (Entity row maps live in `04_persistence`.) ## 1. High-Level Overview **Purpose**: Own the **mission lifecycle** for an edge deployment. A "mission" is a planned record with a name, creation timestamp, and assigned vehicle (Plane / Copter / UGV / GuidedMissile); "waypoints" are the ordered geo-points (with altitude, source, and objective) that define the mission's route. This component is consumed by `autopilot` (reads the mission and waypoints to drive the vehicle) and by the `ui` (map view + planning UI). **Architectural pattern**: Aggregate root (`Mission`) with a sub-aggregate (`Waypoint`). Manual cascade-delete -- schema declares plain `REFERENCES` (no `ON DELETE CASCADE`); this service walks the dependency graph by hand. **Cross-service contract -- the cascade**: when a mission or waypoint is deleted, this service **also** tears down rows in tables it does NOT own the schema for: `media` + `annotations` (owned by the `annotations` service) and `detection` (owned by the detection pipeline), plus its own `map_objects`. Per `../../../suite/_docs/02_missions.md` §5 + §9 this is the canonical, spec-defined behavior -- this service is the only place that knows the full mission ownership graph and is contractually responsible for this cleanup. The shared local PostgreSQL on the edge device makes the multi-table cascade physically possible in one connection. **Removed from cascade in B7**: `orthophotos` and `gps_corrections`. Those tables now live in the separate `gps-denied` service per `../../../suite/_docs/11_gps_denied.md`. `MissionService.DeleteMission` and `WaypointService.DeleteWaypoint` no longer reference them. **Upstream dependencies**: `05_identity` (`[Authorize FL]`), `04_persistence`, `06_http_conventions`, `01_vehicle_catalog` (existence check on `vehicle_id`). **Downstream consumers** (live runtime): `autopilot` (reads missions + waypoints), `ui` (planning + map). The new `gps-denied` service references `mission_id` and `waypoint_id` from its own tables but does NOT depend on this service at runtime; cleanup of its rows is its own concern. ## 2. Internal Interface ```csharp public class MissionService(AppDataConnection db) { Task CreateMission(CreateMissionRequest); Task UpdateMission(Guid id, UpdateMissionRequest); Task GetMission(Guid id); Task> GetMissions(GetMissionsQuery); Task DeleteMission(Guid id); // cross-service cascade } public class WaypointService(AppDataConnection db) { Task CreateWaypoint(Guid missionId, CreateWaypointRequest); Task UpdateWaypoint(Guid missionId, Guid waypointId, UpdateWaypointRequest); Task> GetWaypoints(Guid missionId); // unpaginated by spec Task DeleteWaypoint(Guid missionId, Guid waypointId); // cross-service cascade } ``` Throws `KeyNotFoundException` (-> 404), `ArgumentException` (-> 400, when referenced `vehicle_id` doesn't exist). ## 3. External API ### Missions | Spec # | Endpoint | Method | Auth | Description | |--------|----------|--------|------|-------------| | 1 | `/missions` | POST | `FL` | Create. Body `CreateMissionRequest`. Code throws `ArgumentException -> 400` if `VehicleId` doesn't exist; spec says 404 -- minor divergence. | | 2 | `/missions/{id:guid}` | PUT | `FL` | Partial update (Name and/or VehicleId). | | 7 | `/missions/{id:guid}` | GET | `FL` | Single by id. | | 8 | `/missions` | GET | `FL` | Paginated list. Query: `Name?`, `FromDate?`, `ToDate?`, `Page=1`, `PageSize=20`. Returns `PaginatedResponse` (envelope from `06_http_conventions`). | | 9 | `/missions/{id:guid}` | DELETE | `FL` | Cascade-deletes waypoints, media, annotations, detection, map_objects (in dependency order). | ### Waypoints (nested under mission) | Spec # | Endpoint | Method | Auth | Description | |--------|----------|--------|------|-------------| | 3 | `/missions/{id:guid}/waypoints` | POST | `FL` | Create. Body `CreateWaypointRequest`. 404 if mission missing. | | 4 | `/missions/{id:guid}/waypoints/{wpId:guid}` | PUT | `FL` | **Full overwrite** of all waypoint fields (Caveats #2 -- diverges from partial-update intent). | | 5 | `/missions/{id:guid}/waypoints/{wpId:guid}` | DELETE | `FL` | Cascade-deletes related media, annotations, detection. | | 6 | `/missions/{id:guid}/waypoints` | GET | `FL` | Ordered by `OrderNum`. Unpaginated (matches spec). | Wire shape: PascalCase (suite-wide divergence -- see `06_http_conventions`). ## 4. Data Access Patterns ### Read queries | Query | Frequency | Hot Path | Index | |-------|-----------|----------|-------| | `missions WHERE id = ?` | Every read/update/delete | Yes | PK ✓ | | `missions WHERE ... ORDER BY created_date DESC LIMIT N OFFSET M` (+ count) | Listing | Yes | None on `created_date` -- could be added | | `vehicles WHERE id = ?` (existence) | Every mission create / update with vehicle change | Yes | PK ✓ (cross-component) | | `waypoints WHERE mission_id = ? AND id = ?` | Per-waypoint read/update/delete | Yes | PK + `ix_waypoints_mission_id` ✓ | | `waypoints WHERE mission_id = ? ORDER BY order_num` | Nested list | Medium | `ix_waypoints_mission_id` (sort still in-memory) | ### Cascade-delete writes (`MissionService.DeleteMission`) In strict dependency order: 1. `DELETE FROM map_objects WHERE mission_id = ?` (autopilot-written, owned-here schema) 2. Resolve `waypointIds = SELECT id FROM waypoints WHERE mission_id = ?` 3. If any: resolve `mediaIds`, `annotationIds`, then `DELETE FROM detection`, `DELETE FROM annotations`, `DELETE FROM media` *(cross-service tables -- schema owned by annotations + detection pipeline)* 4. `DELETE FROM waypoints WHERE mission_id = ?` 5. `DELETE FROM missions WHERE id = ?` `WaypointService.DeleteWaypoint` does the equivalent steps 2-4 scoped to one waypoint. **No transaction wraps either cascade** -- partial failure leaves orphan rows. Tracked as Caveat #1; would be a one-line fix (`db.BeginTransactionAsync()`). ### Caching None. ### Storage Estimates Not specified. ## 5. Implementation Details **State Management**: Stateless services. **Key business rules**: - `mission.vehicle_id` must reference an existing vehicle (validated on create + on update if changed). - `waypoint.mission_id` must reference an existing mission (validated on create). - Cascade tables must be deleted in child-before-parent order due to FK constraints. **Error Handling**: Services throw; `06_http_conventions` middleware maps. ## 6. Extensions and Helpers `PaginatedResponse` (defined in `06_http_conventions`) is consumed only by this component. ## 7. Caveats & Edge Cases 1. **No transaction around cascade delete** -- partial failure orphans rows in `media`, `annotations`, `detection`, `map_objects`, or `waypoints`. Wrapping in `db.BeginTransactionAsync()` is one extra line and would make the cascade atomic. 2. **`UpdateWaypoint` overwrites all fields** even though the request looks "partial-shaped" -- sending `{}` zeroes out coordinates and resets enums. Spec §4 also overwrites all fields, but spec uses the auto-converting `Geopoint` type so a missing `Geopoint` would be `null` not zero. With code's 3-flat-fields shape, this is more error-prone. 3. **Geopoint shape divergence from spec**: spec defines a single `string GPS` with auto-conversion (`Lat <-> MGRS`). Code uses 3 separate columns with no conversion. Carries through `Waypoint`, `MapObject`, and the request DTOs. 4. **Vehicle existence check + mission insert is non-transactional** -- TOCTOU window for vehicle delete is mitigated by the FK (which would reject the insert), but the error UX would surface as a 500 instead of a 400 in that race. 5. **No reorder endpoint** -- N waypoints reordered = N PUTs, racy. 6. **Cascade depends on cross-service tables existing in the same DB.** In standard edge deployment this is guaranteed (annotations/detection migrate them in the same compose stack, same `postgres-local`). In any deployment where those services are absent, the cascade will throw `relation does not exist`. 7. **Entity returned on the wire** with `[Association]` properties (`Mission.Vehicle`, `Mission.Waypoints`, `Waypoint.Mission`); LinqToDB does NOT eager-load by default on `FirstOrDefaultAsync(predicate)`, so they serialize as `null` / `[]`. Verify in Step 4 against actual responses. 8. **Spec §1 says 404 on missing VehicleId**; code throws `ArgumentException` which maps to **400**. Minor divergence. ## 8. Dependency Graph **Must be implemented after**: `05_identity`, `06_http_conventions`, `04_persistence`, `01_vehicle_catalog`. **Blocks**: `07_host`. ## 9. Logging Strategy No app-level logs.