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

19 KiB
Raw Blame History

Phase 2: Enhanced Business Logic

Overview

Implement sophisticated business logic for fuel type/grade relationships, Imperial/Metric conversion system, enhanced MPG calculations, and advanced validation rules.

Prerequisites

  • Phase 1 completed (database schema and core types)
  • Database migration deployed and tested
  • Core validation logic functional

Fuel Type/Grade Dynamic System

Fuel Grade Service

File: backend/src/features/fuel-logs/domain/fuel-grade.service.ts

import { FuelType, FuelGrade, GasolineFuelGrade, DieselFuelGrade } from './fuel-logs.types';

export interface FuelGradeOption {
  value: FuelGrade;
  label: string;
  description?: string;
}

export class FuelGradeService {
  
  static getFuelGradeOptions(fuelType: FuelType): FuelGradeOption[] {
    switch (fuelType) {
      case FuelType.GASOLINE:
        return [
          { value: GasolineFuelGrade.REGULAR_87, label: '87 (Regular)', description: 'Regular unleaded gasoline' },
          { value: GasolineFuelGrade.MIDGRADE_88, label: '88 (Mid-Grade)', description: 'Mid-grade gasoline' },
          { value: GasolineFuelGrade.MIDGRADE_89, label: '89 (Mid-Grade Plus)', description: 'Mid-grade plus gasoline' },
          { value: GasolineFuelGrade.PREMIUM_91, label: '91 (Premium)', description: 'Premium gasoline' },
          { value: GasolineFuelGrade.PREMIUM_93, label: '93 (Premium Plus)', description: 'Premium plus gasoline' }
        ];
      
      case FuelType.DIESEL:
        return [
          { value: DieselFuelGrade.DIESEL_1, label: '#1 Diesel', description: 'Light diesel fuel' },
          { value: DieselFuelGrade.DIESEL_2, label: '#2 Diesel', description: 'Standard diesel fuel' }
        ];
      
      case FuelType.ELECTRIC:
        return []; // No grades for electric
      
      default:
        return [];
    }
  }
  
  static isValidGradeForFuelType(fuelType: FuelType, fuelGrade?: FuelGrade): boolean {
    if (!fuelGrade) {
      return fuelType === FuelType.ELECTRIC; // Only electric allows null grade
    }
    
    const validGrades = this.getFuelGradeOptions(fuelType).map(option => option.value);
    return validGrades.includes(fuelGrade);
  }
  
  static getDefaultGrade(fuelType: FuelType): FuelGrade {
    switch (fuelType) {
      case FuelType.GASOLINE:
        return GasolineFuelGrade.REGULAR_87;
      case FuelType.DIESEL:
        return DieselFuelGrade.DIESEL_2;
      case FuelType.ELECTRIC:
        return null;
      default:
        return null;
    }
  }
}

Imperial/Metric Conversion System

Unit Conversion Service

File: backend/src/features/fuel-logs/domain/unit-conversion.service.ts

import { UnitSystem, UnitConversion } from './fuel-logs.types';

export interface ConversionFactors {
  // Volume conversions
  gallonsToLiters: number;
  litersToGallons: number;
  
  // Distance conversions  
  milesToKilometers: number;
  kilometersToMiles: number;
}

export class UnitConversionService {
  
  private static readonly FACTORS: ConversionFactors = {
    gallonsToLiters: 3.78541,
    litersToGallons: 0.264172,
    milesToKilometers: 1.60934,
    kilometersToMiles: 0.621371
  };
  
  static getUnitLabels(unitSystem: UnitSystem): UnitConversion {
    switch (unitSystem) {
      case UnitSystem.IMPERIAL:
        return {
          fuelUnits: 'gallons',
          distanceUnits: 'miles',
          efficiencyUnits: 'mpg'
        };
      case UnitSystem.METRIC:
        return {
          fuelUnits: 'liters',
          distanceUnits: 'kilometers', 
          efficiencyUnits: 'L/100km'
        };
    }
  }
  
  // Volume conversions
  static convertFuelUnits(value: number, fromSystem: UnitSystem, toSystem: UnitSystem): number {
    if (fromSystem === toSystem) return value;
    
    if (fromSystem === UnitSystem.IMPERIAL && toSystem === UnitSystem.METRIC) {
      return value * this.FACTORS.gallonsToLiters; // gallons to liters
    }
    
    if (fromSystem === UnitSystem.METRIC && toSystem === UnitSystem.IMPERIAL) {
      return value * this.FACTORS.litersToGallons; // liters to gallons
    }
    
    return value;
  }
  
  // Distance conversions
  static convertDistance(value: number, fromSystem: UnitSystem, toSystem: UnitSystem): number {
    if (fromSystem === toSystem) return value;
    
    if (fromSystem === UnitSystem.IMPERIAL && toSystem === UnitSystem.METRIC) {
      return value * this.FACTORS.milesToKilometers; // miles to kilometers
    }
    
    if (fromSystem === UnitSystem.METRIC && toSystem === UnitSystem.IMPERIAL) {
      return value * this.FACTORS.kilometersToMiles; // kilometers to miles
    }
    
    return value;
  }
  
  // Efficiency calculations
  static calculateEfficiency(distance: number, fuelUnits: number, unitSystem: UnitSystem): number {
    if (fuelUnits <= 0) return 0;
    
    switch (unitSystem) {
      case UnitSystem.IMPERIAL:
        return distance / fuelUnits; // miles per gallon
      case UnitSystem.METRIC:
        return (fuelUnits / distance) * 100; // liters per 100 kilometers
      default:
        return 0;
    }
  }
  
  // Convert efficiency between unit systems
  static convertEfficiency(efficiency: number, fromSystem: UnitSystem, toSystem: UnitSystem): number {
    if (fromSystem === toSystem) return efficiency;
    
    if (fromSystem === UnitSystem.IMPERIAL && toSystem === UnitSystem.METRIC) {
      // MPG to L/100km: L/100km = 235.214 / MPG
      return efficiency > 0 ? 235.214 / efficiency : 0;
    }
    
    if (fromSystem === UnitSystem.METRIC && toSystem === UnitSystem.IMPERIAL) {
      // L/100km to MPG: MPG = 235.214 / (L/100km)
      return efficiency > 0 ? 235.214 / efficiency : 0;
    }
    
    return efficiency;
  }
}

Enhanced MPG/Efficiency Calculations

Efficiency Calculation Service

File: backend/src/features/fuel-logs/domain/efficiency-calculation.service.ts

import { FuelLog, UnitSystem } from './fuel-logs.types';
import { UnitConversionService } from './unit-conversion.service';

export interface EfficiencyResult {
  value: number;
  unitSystem: UnitSystem;
  label: string;
  calculationMethod: 'odometer' | 'trip_distance';
}

export class EfficiencyCalculationService {
  
  /**
   * Calculate efficiency for a fuel log entry
   */
  static calculateEfficiency(
    currentLog: Partial<FuelLog>,
    previousLog: FuelLog | null,
    userUnitSystem: UnitSystem
  ): EfficiencyResult | null {
    
    // Determine calculation method and distance
    let distance: number;
    let calculationMethod: 'odometer' | 'trip_distance';
    
    if (currentLog.tripDistance) {
      // Use trip distance directly
      distance = currentLog.tripDistance;
      calculationMethod = 'trip_distance';
    } else if (currentLog.odometerReading && previousLog?.odometerReading) {
      // Calculate from odometer difference
      distance = currentLog.odometerReading - previousLog.odometerReading;
      calculationMethod = 'odometer';
      
      if (distance <= 0) {
        return null; // Invalid distance
      }
    } else {
      return null; // Cannot calculate efficiency
    }
    
    if (!currentLog.fuelUnits || currentLog.fuelUnits <= 0) {
      return null; // Invalid fuel amount
    }
    
    // Calculate efficiency in user's preferred unit system
    const efficiency = UnitConversionService.calculateEfficiency(
      distance,
      currentLog.fuelUnits,
      userUnitSystem
    );
    
    const unitLabels = UnitConversionService.getUnitLabels(userUnitSystem);
    
    return {
      value: efficiency,
      unitSystem: userUnitSystem,
      label: unitLabels.efficiencyUnits,
      calculationMethod
    };
  }
  
  /**
   * Calculate average efficiency for a set of fuel logs
   */
  static calculateAverageEfficiency(
    fuelLogs: FuelLog[],
    userUnitSystem: UnitSystem
  ): EfficiencyResult | null {
    
    const validLogs = fuelLogs.filter(log => log.mpg && log.mpg > 0);
    
    if (validLogs.length === 0) {
      return null;
    }
    
    // Convert all efficiencies to user's unit system and average
    const efficiencies = validLogs.map(log => {
      // Assume stored efficiency is in Imperial (MPG)
      return UnitConversionService.convertEfficiency(
        log.mpg!,
        UnitSystem.IMPERIAL,
        userUnitSystem
      );
    });
    
    const averageEfficiency = efficiencies.reduce((sum, eff) => sum + eff, 0) / efficiencies.length;
    const unitLabels = UnitConversionService.getUnitLabels(userUnitSystem);
    
    return {
      value: averageEfficiency,
      unitSystem: userUnitSystem,
      label: unitLabels.efficiencyUnits,
      calculationMethod: 'odometer' // Mixed, but default to odometer
    };
  }
  
  /**
   * Calculate total distance traveled from fuel logs
   */
  static calculateTotalDistance(fuelLogs: FuelLog[], userUnitSystem: UnitSystem): number {
    let totalDistance = 0;
    
    for (let i = 1; i < fuelLogs.length; i++) {
      const current = fuelLogs[i];
      const previous = fuelLogs[i - 1];
      
      if (current.tripDistance) {
        // Use trip distance if available
        totalDistance += current.tripDistance;
      } else if (current.odometerReading && previous.odometerReading) {
        // Calculate from odometer difference
        const distance = current.odometerReading - previous.odometerReading;
        if (distance > 0) {
          totalDistance += distance;
        }
      }
    }
    
    return totalDistance;
  }
}

Advanced Validation Rules

Enhanced Validation Service

File: backend/src/features/fuel-logs/domain/enhanced-validation.service.ts

import { CreateFuelLogRequest, UpdateFuelLogRequest, FuelType, UnitSystem } from './fuel-logs.types';
import { FuelGradeService } from './fuel-grade.service';

export interface ValidationResult {
  isValid: boolean;
  errors: string[];
  warnings: string[];
}

export class EnhancedValidationService {
  
  static validateFuelLogData(
    data: CreateFuelLogRequest | UpdateFuelLogRequest,
    userUnitSystem: UnitSystem
  ): ValidationResult {
    
    const errors: string[] = [];
    const warnings: string[] = [];
    
    // Distance requirement validation
    this.validateDistanceRequirement(data, errors);
    
    // Fuel system validation
    this.validateFuelSystem(data, errors);
    
    // Numeric value validation
    this.validateNumericValues(data, errors, warnings);
    
    // DateTime validation
    this.validateDateTime(data, errors);
    
    // Business logic validation
    this.validateBusinessRules(data, errors, warnings, userUnitSystem);
    
    return {
      isValid: errors.length === 0,
      errors,
      warnings
    };
  }
  
  private static validateDistanceRequirement(
    data: CreateFuelLogRequest | UpdateFuelLogRequest,
    errors: string[]
  ): void {
    const hasOdometer = data.odometerReading && data.odometerReading > 0;
    const hasTripDistance = data.tripDistance && data.tripDistance > 0;
    
    if (!hasOdometer && !hasTripDistance) {
      errors.push('Either odometer reading or trip distance is required');
    }
    
    if (hasOdometer && hasTripDistance) {
      errors.push('Cannot specify both odometer reading and trip distance');
    }
  }
  
  private static validateFuelSystem(
    data: CreateFuelLogRequest | UpdateFuelLogRequest,
    errors: string[]
  ): void {
    if (!data.fuelType) return;
    
    // Validate fuel type
    if (!Object.values(FuelType).includes(data.fuelType)) {
      errors.push(`Invalid fuel type: ${data.fuelType}`);
      return;
    }
    
    // Validate fuel grade for fuel type
    if (!FuelGradeService.isValidGradeForFuelType(data.fuelType, data.fuelGrade)) {
      errors.push(`Invalid fuel grade '${data.fuelGrade}' for fuel type '${data.fuelType}'`);
    }
  }
  
  private static validateNumericValues(
    data: CreateFuelLogRequest | UpdateFuelLogRequest,
    errors: string[],
    warnings: string[]
  ): void {
    
    // Positive value checks
    if (data.fuelUnits !== undefined && data.fuelUnits <= 0) {
      errors.push('Fuel units must be positive');
    }
    
    if (data.costPerUnit !== undefined && data.costPerUnit <= 0) {
      errors.push('Cost per unit must be positive');
    }
    
    if (data.odometerReading !== undefined && data.odometerReading <= 0) {
      errors.push('Odometer reading must be positive');
    }
    
    if (data.tripDistance !== undefined && data.tripDistance <= 0) {
      errors.push('Trip distance must be positive');
    }
    
    // Reasonable value warnings
    if (data.fuelUnits && data.fuelUnits > 100) {
      warnings.push('Fuel amount seems unusually high (>100 units)');
    }
    
    if (data.costPerUnit && data.costPerUnit > 10) {
      warnings.push('Cost per unit seems unusually high (>$10)');
    }
    
    if (data.tripDistance && data.tripDistance > 1000) {
      warnings.push('Trip distance seems unusually high (>1000 miles)');
    }
  }
  
  private static validateDateTime(
    data: CreateFuelLogRequest | UpdateFuelLogRequest,
    errors: string[]
  ): void {
    if (!data.dateTime) return;
    
    const date = new Date(data.dateTime);
    const now = new Date();
    
    if (isNaN(date.getTime())) {
      errors.push('Invalid date/time format');
      return;
    }
    
    if (date > now) {
      errors.push('Cannot create fuel logs in the future');
    }
    
    // Check if date is too far in the past (>2 years)
    const twoYearsAgo = new Date(now.getTime() - (2 * 365 * 24 * 60 * 60 * 1000));
    if (date < twoYearsAgo) {
      errors.push('Fuel log date cannot be more than 2 years in the past');
    }
  }
  
  private static validateBusinessRules(
    data: CreateFuelLogRequest | UpdateFuelLogRequest,
    errors: string[],
    warnings: string[],
    userUnitSystem: UnitSystem
  ): void {
    
    // Electric vehicle specific validation
    if (data.fuelType === FuelType.ELECTRIC) {
      if (data.costPerUnit && data.costPerUnit > 0.50) {
        warnings.push('Cost per kWh seems high for electric charging');
      }
    }
    
    // Efficiency warning calculation
    if (data.fuelUnits && data.tripDistance) {
      const estimatedMPG = data.tripDistance / data.fuelUnits;
      
      if (userUnitSystem === UnitSystem.IMPERIAL) {
        if (estimatedMPG < 5) {
          warnings.push('Calculated efficiency is very low (<5 MPG)');
        } else if (estimatedMPG > 50) {
          warnings.push('Calculated efficiency is very high (>50 MPG)');
        }
      }
    }
    
    // Cost validation
    if (data.fuelUnits && data.costPerUnit) {
      const calculatedTotal = data.fuelUnits * data.costPerUnit;
      // Allow 1 cent tolerance for rounding
      if (Math.abs(calculatedTotal - (data.totalCost || calculatedTotal)) > 0.01) {
        warnings.push('Total cost does not match fuel units × cost per unit');
      }
    }
  }
}

User Settings Integration

User Settings Service Interface

File: backend/src/features/fuel-logs/external/user-settings.service.ts

import { UnitSystem } from '../domain/fuel-logs.types';

export interface UserSettings {
  unitSystem: UnitSystem;
  defaultFuelType?: string;
  currencyCode: string;
  timeZone: string;
}

export class UserSettingsService {
  
  /**
   * Get user's unit system preference
   * TODO: Integrate with actual user settings service
   */
  static async getUserUnitSystem(userId: string): Promise<UnitSystem> {
    // Placeholder implementation - replace with actual user settings lookup
    // For now, default to Imperial
    return UnitSystem.IMPERIAL;
  }
  
  /**
   * Get full user settings for fuel logs
   */
  static async getUserSettings(userId: string): Promise<UserSettings> {
    // Placeholder implementation
    return {
      unitSystem: await this.getUserUnitSystem(userId),
      currencyCode: 'USD',
      timeZone: 'America/New_York'
    };
  }
  
  /**
   * Update user's unit system preference
   */
  static async updateUserUnitSystem(userId: string, unitSystem: UnitSystem): Promise<void> {
    // Placeholder implementation - replace with actual user settings update
    console.log(`Update user ${userId} unit system to ${unitSystem}`);
  }
}

Implementation Tasks

Fuel Type/Grade System

  1. Create FuelGradeService with dynamic grade options
  2. Implement fuel type validation logic
  3. Add default grade selection
  4. Create grade validation for each fuel type

Unit Conversion System

  1. Create UnitConversionService with conversion factors
  2. Implement volume/distance conversions
  3. Add efficiency calculation methods
  4. Create unit label management

Enhanced Calculations

  1. Create EfficiencyCalculationService
  2. Implement trip distance vs odometer logic
  3. Add average efficiency calculations
  4. Create total distance calculations

Advanced Validation

  1. Create EnhancedValidationService
  2. Implement comprehensive validation rules
  3. Add business logic validation
  4. Create warning system for unusual values

User Settings Integration

  1. Create UserSettingsService interface
  2. Add unit system preference lookup
  3. Prepare for actual user settings integration

Testing Requirements

Unit Tests Required

// Test fuel grade service
describe('FuelGradeService', () => {
  it('should return correct grades for gasoline', () => {
    const grades = FuelGradeService.getFuelGradeOptions(FuelType.GASOLINE);
    expect(grades).toHaveLength(5);
    expect(grades[0].value).toBe('87');
  });
  
  it('should validate grades correctly', () => {
    expect(FuelGradeService.isValidGradeForFuelType(FuelType.GASOLINE, '87')).toBe(true);
    expect(FuelGradeService.isValidGradeForFuelType(FuelType.GASOLINE, '#1')).toBe(false);
  });
});

// Test unit conversion service
describe('UnitConversionService', () => {
  it('should convert gallons to liters correctly', () => {
    const liters = UnitConversionService.convertFuelUnits(10, UnitSystem.IMPERIAL, UnitSystem.METRIC);
    expect(liters).toBeCloseTo(37.85, 2);
  });
  
  it('should calculate MPG correctly', () => {
    const mpg = UnitConversionService.calculateEfficiency(300, 10, UnitSystem.IMPERIAL);
    expect(mpg).toBe(30);
  });
});

// Test efficiency calculation service
describe('EfficiencyCalculationService', () => {
  it('should calculate efficiency from trip distance', () => {
    const result = EfficiencyCalculationService.calculateEfficiency(
      { tripDistance: 300, fuelUnits: 10 },
      null,
      UnitSystem.IMPERIAL
    );
    expect(result?.value).toBe(30);
    expect(result?.calculationMethod).toBe('trip_distance');
  });
});

// Test validation service
describe('EnhancedValidationService', () => {
  it('should require distance input', () => {
    const result = EnhancedValidationService.validateFuelLogData(
      { fuelType: FuelType.GASOLINE, fuelUnits: 10, costPerUnit: 3.50 },
      UnitSystem.IMPERIAL
    );
    expect(result.isValid).toBe(false);
    expect(result.errors).toContain('Either odometer reading or trip distance is required');
  });
});

Success Criteria

Phase 2 Complete When:

  • Fuel type/grade system fully functional
  • Imperial/Metric conversions working correctly
  • Enhanced efficiency calculations implemented
  • Advanced validation rules active
  • User settings integration interface ready
  • All business logic unit tested
  • Integration with existing fuel logs service

Ready for Phase 3 When:

  • All business logic services tested and functional
  • Unit conversion system verified accurate
  • Fuel grade system working correctly
  • Validation rules catching all edge cases
  • Ready for API integration

Next Phase: Phase 3 - API & Backend Implementation