# 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` ```typescript 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` ```typescript 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` ```typescript 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, 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` ```typescript 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` ```typescript 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 { // 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 { // 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 { // 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 ```typescript // 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](FUEL-LOGS-PHASE-3.md)