# Module: `Azaion.Missions.Services.VehicleService` **File**: `Services/VehicleService.cs` > **NOTE (forward-looking)**: post-rename. Today's source is `Services/AircraftService.cs` operating over `Aircraft` entities. Renames tracked under Jira AZ-EPIC child B6. ## Purpose Encapsulates vehicle-related domain operations: CRUD plus the "is_default" exclusivity rule. This is the only place the "exactly one vehicle is default" invariant is enforced. ## Public Interface ```csharp public class VehicleService(AppDataConnection db) { Task CreateVehicle(CreateVehicleRequest request); Task UpdateVehicle(Guid id, UpdateVehicleRequest request); Task GetVehicle(Guid id); Task> GetVehicles(GetVehiclesQuery query); Task DeleteVehicle(Guid id); Task SetDefault(Guid id, SetDefaultRequest request); } ``` ## Internal Logic ### `CreateVehicle` 1. If `request.IsDefault` is `true`, **clear `is_default` on every other vehicle first** (`UPDATE vehicles SET is_default = FALSE WHERE is_default = TRUE`). 2. Build a fresh `Vehicle` with `Id = Guid.NewGuid()` and copy every request field 1:1. 3. `db.InsertAsync(vehicle)` and return the persisted entity. ### `UpdateVehicle` 1. Load the row by id; throw `KeyNotFoundException` if not found (mapped to 404 by middleware). 2. For each property on `UpdateVehicleRequest`, apply only the non-null fields (partial update). 3. If `IsDefault.Value == true`, run the same exclusivity clear as `CreateVehicle` BEFORE setting the new default. 4. `db.UpdateAsync(vehicle)`. ### `GetVehicle` - `FirstOrDefaultAsync(v => v.Id == id)` -> 404 on miss. ### `GetVehicles` - Builds a LINQ query on `db.Vehicles`. - Filters: `query.Name` -> case-insensitive `Contains` (uses `string.ToLower()` on both sides -- runs as `LOWER(name) LIKE %lower(input)%` server-side via LinqToDB translation); `query.IsDefault` -> exact equality. - Orders by `Name` ascending. - Returns the full list -- **no pagination** despite `GetVehiclesQuery` being a query object. Spec accepts this (vehicles are a small dataset). ### `DeleteVehicle` 1. Guard: `db.Missions.AnyAsync(m => m.VehicleId == id)` -> if any mission references the vehicle, throw `InvalidOperationException` ("Vehicle is referenced by missions") -> 409 Conflict. 2. Load by id (404 on miss). 3. `db.Vehicles.DeleteAsync(v => v.Id == id)`. ### `SetDefault` - Loads the row (404 on miss). - If `request.IsDefault == true`, clears the flag on every other vehicle, then sets it on the target. - If `request.IsDefault == false`, simply clears the flag on the target -- **no guarantee remains that exactly one vehicle is default**. Behavior is "the user told me to clear the flag, so I clear it". ## "Exactly one default" rule -- spec vs code This is the canonical surviving spec-vs-code divergence in the rename Epic. See Jira **B12** (decision-only ticket). - **Spec** (`../../suite/_docs/02_missions.md`): `PATCH /vehicles/{id}/default` "toggles the default flag on a vehicle." Only the target row changes. - **Code**: when setting `IsDefault = true`, the service first clears the flag on **every other vehicle** in the same connection. Two operations, no transaction. Two real consequences: 1. **Code is stricter than spec** -- in code there can be 0 or 1 default vehicle, never 2+. Spec would allow N defaults if the UI sets multiple. 2. **Race window**: two concurrent "set default = true" calls on different rows could both clear-then-set, leaving two defaults. linq2db default behavior is autocommit per statement; no `BeginTransactionAsync` is called. B12 either lifts the rule into spec + wraps in a transaction (preferred), or drops the side-effect from code and lets UI handle exclusivity. The race fix is part of either resolution. ## Dependencies - `Azaion.Missions.Database.AppDataConnection` - `Azaion.Missions.Database.Entities.Vehicle` - `Azaion.Missions.Database.Entities.Mission` (queried via `db.Missions` in `DeleteVehicle`) - `Azaion.Missions.DTOs` (request/query DTOs) ## Consumers - `Controllers.VehiclesController` -- wraps every method 1:1 with an HTTP route. ## Data Models Reads/writes only `vehicles` table; reads (existence check) `missions` table. ## Configuration / External Integrations None. ## Security - All endpoints in the controller carry `[Authorize(Policy = "FL")]`, so this service is unreachable without a JWT bearing `permissions=FL`. - Exception messages include the supplied id (`"Vehicle {id} not found"`) -- no PII risk for GUIDs. ## Tests None present. ## Notes / Smells 1. **`GetVehicles` ignores pagination** by design -- the dataset is small (a fleet, not a catalog of millions). Inconsistent listing contract with `MissionService.GetMissions` but intentional. 2. **"Exactly one default" race** -- see B12 above. 3. **Delete fails fast on referenced vehicle** but does NOT cascade-delete or null-out the `vehicle_id` -- strictly a 409 to the caller. Consistent with the schema: `missions.vehicle_id` is `NOT NULL REFERENCES vehicles(id)` with no `ON DELETE` clause (PostgreSQL defaults to `NO ACTION`). 4. **Case-insensitive search** uses `string.ToLower()` on both sides, which LinqToDB renders as `LOWER(...)` -- non-indexed; full-table scan on large datasets. Fine at fleet size.