using System.Data; using Azaion.Missions.Database; using Azaion.Missions.Database.Entities; using Azaion.Missions.DTOs; namespace Azaion.Missions.Services; public class VehicleService(AppDataConnection db) { // B12 (Option A): "exactly one default vehicle" is the user-visible truth. // Every code path that sets is_default=true clears existing defaults and // assigns the new default inside a Serializable transaction so two // concurrent default-set ops cannot leave 0 or 2 defaults. The DB-level // partial unique index `ux_vehicles_one_default` (DatabaseMigrator) is the // belt-and-braces backstop if a future code path forgets the transaction. public async Task CreateVehicle(CreateVehicleRequest request) { var vehicle = new Vehicle { Id = Guid.NewGuid(), Type = request.Type, Model = request.Model, Name = request.Name, FuelType = request.FuelType, BatteryCapacity = request.BatteryCapacity, EngineConsumption = request.EngineConsumption, EngineConsumptionIdle = request.EngineConsumptionIdle, IsDefault = request.IsDefault }; if (request.IsDefault) { await using var tx = await db.BeginTransactionAsync(IsolationLevel.Serializable); await db.Vehicles.Where(v => v.IsDefault).Set(v => v.IsDefault, false).UpdateAsync(); await db.InsertAsync(vehicle); await tx.CommitAsync(); } else { await db.InsertAsync(vehicle); } return vehicle; } public async Task UpdateVehicle(Guid id, UpdateVehicleRequest request) { var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id) ?? throw new KeyNotFoundException($"Vehicle {id} not found"); if (request.Type.HasValue) vehicle.Type = request.Type.Value; if (request.Model != null) vehicle.Model = request.Model; if (request.Name != null) vehicle.Name = request.Name; if (request.FuelType.HasValue) vehicle.FuelType = request.FuelType.Value; if (request.BatteryCapacity.HasValue) vehicle.BatteryCapacity = request.BatteryCapacity.Value; if (request.EngineConsumption.HasValue) vehicle.EngineConsumption = request.EngineConsumption.Value; if (request.EngineConsumptionIdle.HasValue) vehicle.EngineConsumptionIdle = request.EngineConsumptionIdle.Value; if (request.IsDefault is true) { await using var tx = await db.BeginTransactionAsync(IsolationLevel.Serializable); await db.Vehicles.Where(v => v.IsDefault && v.Id != id).Set(v => v.IsDefault, false).UpdateAsync(); vehicle.IsDefault = true; await db.UpdateAsync(vehicle); await tx.CommitAsync(); } else { if (request.IsDefault is false) vehicle.IsDefault = false; await db.UpdateAsync(vehicle); } return vehicle; } public async Task GetVehicle(Guid id) { var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id) ?? throw new KeyNotFoundException($"Vehicle {id} not found"); return vehicle; } public async Task> GetVehicles(GetVehiclesQuery query) { var q = db.Vehicles.AsQueryable(); if (!string.IsNullOrEmpty(query.Name)) q = q.Where(v => v.Name.ToLower().Contains(query.Name.ToLower())); if (query.IsDefault.HasValue) q = q.Where(v => v.IsDefault == query.IsDefault.Value); return await q.OrderBy(v => v.Name).ToListAsync(); } public async Task DeleteVehicle(Guid id) { var hasMissions = await db.Missions.AnyAsync(m => m.VehicleId == id); if (hasMissions) throw new InvalidOperationException($"Vehicle {id} is referenced by missions"); var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id) ?? throw new KeyNotFoundException($"Vehicle {id} not found"); await db.Vehicles.DeleteAsync(v => v.Id == id); } public async Task SetDefault(Guid id, SetDefaultRequest request) { var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id) ?? throw new KeyNotFoundException($"Vehicle {id} not found"); if (request.IsDefault) { await using var tx = await db.BeginTransactionAsync(IsolationLevel.Serializable); await db.Vehicles.Where(v => v.IsDefault && v.Id != id).Set(v => v.IsDefault, false).UpdateAsync(); vehicle.IsDefault = true; await db.UpdateAsync(vehicle); await tx.CommitAsync(); } else { vehicle.IsDefault = false; await db.UpdateAsync(vehicle); } } }