Files
motovaultpro/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-1.md
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

12 KiB

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

-- 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

-- 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

-- 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

// 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

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

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

-- 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