19 KiB
19 KiB
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
- ✅ Create FuelGradeService with dynamic grade options
- ✅ Implement fuel type validation logic
- ✅ Add default grade selection
- ✅ Create grade validation for each fuel type
Unit Conversion System
- ✅ Create UnitConversionService with conversion factors
- ✅ Implement volume/distance conversions
- ✅ Add efficiency calculation methods
- ✅ Create unit label management
Enhanced Calculations
- ✅ Create EfficiencyCalculationService
- ✅ Implement trip distance vs odometer logic
- ✅ Add average efficiency calculations
- ✅ Create total distance calculations
Advanced Validation
- ✅ Create EnhancedValidationService
- ✅ Implement comprehensive validation rules
- ✅ Add business logic validation
- ✅ Create warning system for unusual values
User Settings Integration
- ✅ Create UserSettingsService interface
- ✅ Add unit system preference lookup
- ✅ 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