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

658 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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`
```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<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
```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)