# Phase 1: Database Schema & Core Logic ## Overview Establish the database foundation for enhanced fuel logs with new fields, validation rules, and core type system updates. ## Prerequisites - Existing fuel logs feature (basic implementation) - PostgreSQL database with current `fuel_logs` table - Migration system functional ## Database Schema Changes ### New Fields to Add ```sql -- Add these columns to fuel_logs table ALTER TABLE fuel_logs ADD COLUMN trip_distance INTEGER; -- Alternative to odometer reading ALTER TABLE fuel_logs ADD COLUMN fuel_type VARCHAR(20) NOT NULL DEFAULT 'gasoline'; ALTER TABLE fuel_logs ADD COLUMN fuel_grade VARCHAR(10); ALTER TABLE fuel_logs ADD COLUMN fuel_units DECIMAL(8,3); -- Replaces gallons for metric support ALTER TABLE fuel_logs ADD COLUMN cost_per_unit DECIMAL(6,3); -- Replaces price_per_gallon ALTER TABLE fuel_logs ADD COLUMN location_data JSONB; -- Future Google Maps integration ALTER TABLE fuel_logs ADD COLUMN date_time TIMESTAMP WITH TIME ZONE; -- Enhanced date/time -- Add constraints ALTER TABLE fuel_logs ADD CONSTRAINT fuel_type_check CHECK (fuel_type IN ('gasoline', 'diesel', 'electric')); -- Add conditional constraint: either trip_distance OR odometer_reading required ALTER TABLE fuel_logs ADD CONSTRAINT distance_required_check CHECK ((trip_distance IS NOT NULL AND trip_distance > 0) OR (odometer_reading IS NOT NULL AND odometer_reading > 0)); -- Add indexes for performance CREATE INDEX idx_fuel_logs_fuel_type ON fuel_logs(fuel_type); CREATE INDEX idx_fuel_logs_date_time ON fuel_logs(date_time); ``` ### Migration Strategy #### Step 1: Additive Migration **File**: `backend/src/features/fuel-logs/migrations/002_enhance_fuel_logs_schema.sql` ```sql -- Migration: 002_enhance_fuel_logs_schema.sql BEGIN; -- Add new columns (nullable initially for data migration) ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS trip_distance INTEGER; ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS fuel_type VARCHAR(20); ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS fuel_grade VARCHAR(10); ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS fuel_units DECIMAL(8,3); ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS cost_per_unit DECIMAL(6,3); ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS location_data JSONB; ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS date_time TIMESTAMP WITH TIME ZONE; -- Migrate existing data UPDATE fuel_logs SET fuel_type = 'gasoline', fuel_units = gallons, cost_per_unit = price_per_gallon, date_time = date::timestamp + interval '12 hours' -- Default to noon WHERE fuel_type IS NULL; -- Add constraints after data migration ALTER TABLE fuel_logs ALTER COLUMN fuel_type SET NOT NULL; ALTER TABLE fuel_logs ALTER COLUMN fuel_type SET DEFAULT 'gasoline'; -- Add check constraints ALTER TABLE fuel_logs ADD CONSTRAINT fuel_type_check CHECK (fuel_type IN ('gasoline', 'diesel', 'electric')); -- Distance requirement constraint (either trip_distance OR odometer_reading) ALTER TABLE fuel_logs ADD CONSTRAINT distance_required_check CHECK ((trip_distance IS NOT NULL AND trip_distance > 0) OR (odometer_reading IS NOT NULL AND odometer_reading > 0)); -- Add performance indexes CREATE INDEX IF NOT EXISTS idx_fuel_logs_fuel_type ON fuel_logs(fuel_type); CREATE INDEX IF NOT EXISTS idx_fuel_logs_date_time ON fuel_logs(date_time); COMMIT; ``` #### Step 2: Backward Compatibility Plan - Keep existing `gallons` and `price_per_gallon` fields during transition - Update application logic to use new fields preferentially - Plan deprecation of old fields in future migration ### Data Validation Rules #### Core Business Rules 1. **Distance Requirement**: Either `trip_distance` OR `odometer_reading` must be provided 2. **Fuel Type Validation**: Must be one of: 'gasoline', 'diesel', 'electric' 3. **Fuel Grade Validation**: Must match fuel type options 4. **Positive Values**: All numeric fields must be > 0 5. **DateTime**: Cannot be in the future #### Fuel Grade Validation Logic ```sql -- Fuel grade validation by type CREATE OR REPLACE FUNCTION validate_fuel_grade() RETURNS TRIGGER AS $$ BEGIN -- Gasoline grades IF NEW.fuel_type = 'gasoline' AND NEW.fuel_grade NOT IN ('87', '88', '89', '91', '93') THEN RAISE EXCEPTION 'Invalid fuel grade % for gasoline', NEW.fuel_grade; END IF; -- Diesel grades IF NEW.fuel_type = 'diesel' AND NEW.fuel_grade NOT IN ('#1', '#2') THEN RAISE EXCEPTION 'Invalid fuel grade % for diesel', NEW.fuel_grade; END IF; -- Electric (no grades) IF NEW.fuel_type = 'electric' AND NEW.fuel_grade IS NOT NULL THEN RAISE EXCEPTION 'Electric fuel type cannot have a grade'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Create trigger CREATE TRIGGER fuel_grade_validation_trigger BEFORE INSERT OR UPDATE ON fuel_logs FOR EACH ROW EXECUTE FUNCTION validate_fuel_grade(); ``` ## TypeScript Type System Updates ### New Core Types **File**: `backend/src/features/fuel-logs/domain/fuel-logs.types.ts` ```typescript // Fuel system enums export enum FuelType { GASOLINE = 'gasoline', DIESEL = 'diesel', ELECTRIC = 'electric' } export enum GasolineFuelGrade { REGULAR_87 = '87', MIDGRADE_88 = '88', MIDGRADE_89 = '89', PREMIUM_91 = '91', PREMIUM_93 = '93' } export enum DieselFuelGrade { DIESEL_1 = '#1', DIESEL_2 = '#2' } export type FuelGrade = GasolineFuelGrade | DieselFuelGrade | null; // Unit system types export enum UnitSystem { IMPERIAL = 'imperial', METRIC = 'metric' } export interface UnitConversion { fuelUnits: string; // 'gallons' | 'liters' distanceUnits: string; // 'miles' | 'kilometers' efficiencyUnits: string; // 'mpg' | 'l/100km' } // Enhanced location data structure export interface LocationData { address?: string; coordinates?: { latitude: number; longitude: number; }; googlePlaceId?: string; stationName?: string; // Future: station prices, fuel availability } // Updated core FuelLog interface export interface FuelLog { id: string; userId: string; vehicleId: string; dateTime: Date; // Enhanced from simple date // Distance tracking (either/or required) odometerReading?: number; tripDistance?: number; // Fuel system fuelType: FuelType; fuelGrade?: FuelGrade; fuelUnits: number; // Replaces gallons costPerUnit: number; // Replaces pricePerGallon totalCost: number; // Auto-calculated // Location (future Google Maps integration) locationData?: LocationData; // Legacy fields (maintain during transition) gallons?: number; // Deprecated pricePerGallon?: number; // Deprecated // Metadata notes?: string; mpg?: number; // Calculated efficiency createdAt: Date; updatedAt: Date; } ``` ### Request/Response Type Updates ```typescript export interface CreateFuelLogRequest { vehicleId: string; dateTime: string; // ISO datetime string // Distance (either required) odometerReading?: number; tripDistance?: number; // Fuel system fuelType: FuelType; fuelGrade?: FuelGrade; fuelUnits: number; costPerUnit: number; // totalCost calculated automatically // Location locationData?: LocationData; notes?: string; } export interface UpdateFuelLogRequest { dateTime?: string; odometerReading?: number; tripDistance?: number; fuelType?: FuelType; fuelGrade?: FuelGrade; fuelUnits?: number; costPerUnit?: number; locationData?: LocationData; notes?: string; } ``` ## Core Validation Logic ### Business Rule Validation **File**: `backend/src/features/fuel-logs/domain/fuel-logs.validation.ts` ```typescript export class FuelLogValidation { static validateDistanceRequirement(data: CreateFuelLogRequest | UpdateFuelLogRequest): void { const hasOdometer = data.odometerReading && data.odometerReading > 0; const hasTripDistance = data.tripDistance && data.tripDistance > 0; if (!hasOdometer && !hasTripDistance) { throw new ValidationError('Either odometer reading or trip distance is required'); } if (hasOdometer && hasTripDistance) { throw new ValidationError('Cannot specify both odometer reading and trip distance'); } } static validateFuelGrade(fuelType: FuelType, fuelGrade?: FuelGrade): void { switch (fuelType) { case FuelType.GASOLINE: if (fuelGrade && !Object.values(GasolineFuelGrade).includes(fuelGrade as GasolineFuelGrade)) { throw new ValidationError(`Invalid gasoline grade: ${fuelGrade}`); } break; case FuelType.DIESEL: if (fuelGrade && !Object.values(DieselFuelGrade).includes(fuelGrade as DieselFuelGrade)) { throw new ValidationError(`Invalid diesel grade: ${fuelGrade}`); } break; case FuelType.ELECTRIC: if (fuelGrade) { throw new ValidationError('Electric vehicles cannot have fuel grades'); } break; } } static validatePositiveValues(data: CreateFuelLogRequest | UpdateFuelLogRequest): void { if (data.fuelUnits && data.fuelUnits <= 0) { throw new ValidationError('Fuel units must be positive'); } if (data.costPerUnit && data.costPerUnit <= 0) { throw new ValidationError('Cost per unit must be positive'); } if (data.odometerReading && data.odometerReading <= 0) { throw new ValidationError('Odometer reading must be positive'); } if (data.tripDistance && data.tripDistance <= 0) { throw new ValidationError('Trip distance must be positive'); } } static validateDateTime(dateTime: string): void { const date = new Date(dateTime); const now = new Date(); if (date > now) { throw new ValidationError('Cannot create fuel logs in the future'); } } } ``` ## Implementation Tasks ### Database Tasks 1. ✅ Create migration file `002_enhance_fuel_logs_schema.sql` 2. ✅ Add new columns with appropriate types 3. ✅ Migrate existing data to new schema 4. ✅ Add database constraints and triggers 5. ✅ Create performance indexes ### Type System Tasks 1. ✅ Define fuel system enums 2. ✅ Create unit system types 3. ✅ Update core FuelLog interface 4. ✅ Update request/response interfaces 5. ✅ Add location data structure ### Validation Tasks 1. ✅ Create validation utility class 2. ✅ Implement distance requirement validation 3. ✅ Implement fuel grade validation 4. ✅ Add positive value checks 5. ✅ Add datetime validation ## Testing Requirements ### Database Testing ```sql -- Test distance requirement constraint INSERT INTO fuel_logs (...) -- Should fail without distance INSERT INTO fuel_logs (trip_distance = 150, ...) -- Should succeed INSERT INTO fuel_logs (odometer_reading = 50000, ...) -- Should succeed INSERT INTO fuel_logs (trip_distance = 150, odometer_reading = 50000, ...) -- Should fail -- Test fuel type/grade validation INSERT INTO fuel_logs (fuel_type = 'gasoline', fuel_grade = '87', ...) -- Should succeed INSERT INTO fuel_logs (fuel_type = 'gasoline', fuel_grade = '#1', ...) -- Should fail INSERT INTO fuel_logs (fuel_type = 'electric', fuel_grade = '87', ...) -- Should fail ``` ### Unit Tests Required - Validation logic for all business rules - Type conversion utilities - Migration data integrity - Constraint enforcement ## Success Criteria ### Phase 1 Complete When: - ✅ Database migration runs successfully - ✅ All new fields available with proper types - ✅ Existing data migrated and preserved - ✅ Database constraints enforce business rules - ✅ TypeScript interfaces updated and compiling - ✅ Core validation logic implemented and tested - ✅ No breaking changes to existing functionality ### Ready for Phase 2 When: - All database changes deployed and tested - Type system fully updated - Core validation passes all tests - Existing fuel logs feature still functional --- **Next Phase**: [Phase 2 - Enhanced Business Logic](FUEL-LOGS-PHASE-2.md)