/** * @ai-summary Enhanced business logic for fuel logs feature * @ai-context Unit-agnostic efficiency and user preferences integration */ import { FuelLogsRepository } from '../data/fuel-logs.repository'; import { EnhancedCreateFuelLogRequest, EnhancedUpdateFuelLogRequest, EnhancedFuelLogResponse, FuelType } from './fuel-logs.types'; import { logger } from '../../../core/logging/logger'; import { cacheService } from '../../../core/config/redis'; import pool from '../../../core/config/database'; import { EnhancedValidationService } from './enhanced-validation.service'; import { UnitConversionService } from './unit-conversion.service'; import { EfficiencyCalculationService } from './efficiency-calculation.service'; import { UserSettingsService } from '../external/user-settings.service'; export class FuelLogsService { private readonly cachePrefix = 'fuel-logs'; private readonly cacheTTL = 300; // 5 minutes constructor(private repository: FuelLogsRepository) {} async createFuelLog(data: EnhancedCreateFuelLogRequest, userId: string): Promise { logger.info('Creating enhanced fuel log', { userId, vehicleId: data.vehicleId, fuelType: data.fuelType }); const userSettings = await UserSettingsService.getUserSettings(userId); const validation = EnhancedValidationService.validateFuelLogData(data); if (!validation.isValid) { throw new Error(validation.errors.join(', ')); } // Verify vehicle ownership const vehicleCheck = await pool.query( 'SELECT id FROM vehicles WHERE id = $1 AND user_id = $2', [data.vehicleId, userId] ); if (vehicleCheck.rows.length === 0) throw new Error('Vehicle not found or unauthorized'); const totalCost = data.fuelUnits * data.costPerUnit; // Previous log for efficiency const prev = data.odometerReading ? await this.repository.getPreviousLogByOdometer(data.vehicleId, data.odometerReading) : await this.repository.getLatestLogForVehicle(data.vehicleId); const eff = EfficiencyCalculationService.calculateEfficiency( { odometerReading: data.odometerReading, tripDistance: data.tripDistance, fuelUnits: data.fuelUnits }, prev?.odometer ?? null, userSettings.unitSystem ); const inserted = await this.repository.createEnhanced({ userId, vehicleId: data.vehicleId, dateTime: new Date(data.dateTime), odometerReading: data.odometerReading, tripDistance: data.tripDistance, fuelType: data.fuelType, fuelGrade: data.fuelGrade ?? null, fuelUnits: data.fuelUnits, costPerUnit: data.costPerUnit, totalCost, locationData: data.locationData ?? null, notes: data.notes }); if (data.odometerReading) { await pool.query( 'UPDATE vehicles SET odometer_reading = $1 WHERE id = $2 AND (odometer_reading IS NULL OR odometer_reading < $1)', [data.odometerReading, data.vehicleId] ); } await this.invalidateCaches(userId, data.vehicleId, userSettings.unitSystem); return this.toEnhancedResponse(inserted, eff?.value ?? undefined, userSettings.unitSystem); } async getFuelLogsByVehicle(vehicleId: string, userId: string): Promise { const vehicleCheck = await pool.query('SELECT id FROM vehicles WHERE id = $1 AND user_id = $2', [vehicleId, userId]); if (vehicleCheck.rows.length === 0) throw new Error('Vehicle not found or unauthorized'); const { unitSystem } = await UserSettingsService.getUserSettings(userId); const cacheKey = `${this.cachePrefix}:vehicle:${vehicleId}:${unitSystem}`; const cached = await cacheService.get(cacheKey); if (cached) return cached; const rows = await this.repository.findByVehicleIdEnhanced(vehicleId); const response = rows.map(r => { const efficiency = EfficiencyCalculationService.calculateEfficiency( { tripDistance: r.trip_distance ?? undefined, fuelUnits: r.fuel_units ?? undefined }, null, // No previous odometer needed for trip distance calculation unitSystem ); return this.toEnhancedResponse(r, efficiency?.value ?? undefined, unitSystem); }); await cacheService.set(cacheKey, response, this.cacheTTL); return response; } async getUserFuelLogs(userId: string): Promise { const { unitSystem } = await UserSettingsService.getUserSettings(userId); const cacheKey = `${this.cachePrefix}:user:${userId}:${unitSystem}`; const cached = await cacheService.get(cacheKey); if (cached) return cached; const rows = await this.repository.findByUserIdEnhanced(userId); const response = rows.map(r => { const efficiency = EfficiencyCalculationService.calculateEfficiency( { tripDistance: r.trip_distance ?? undefined, fuelUnits: r.fuel_units ?? undefined }, null, // No previous odometer needed for trip distance calculation unitSystem ); return this.toEnhancedResponse(r, efficiency?.value ?? undefined, unitSystem); }); await cacheService.set(cacheKey, response, this.cacheTTL); return response; } async getFuelLog(id: string, userId: string): Promise { const row = await this.repository.findByIdEnhanced(id); if (!row) throw new Error('Fuel log not found'); if (row.user_id !== userId) throw new Error('Unauthorized'); const { unitSystem } = await UserSettingsService.getUserSettings(userId); const efficiency = EfficiencyCalculationService.calculateEfficiency( { tripDistance: row.trip_distance ?? undefined, fuelUnits: row.fuel_units ?? undefined }, null, // No previous odometer needed for trip distance calculation unitSystem ); return this.toEnhancedResponse(row, efficiency?.value ?? undefined, unitSystem); } async updateFuelLog(id: string, data: EnhancedUpdateFuelLogRequest, userId: string): Promise { logger.info('Updating enhanced fuel log', { id, userId }); // Verify the fuel log exists and belongs to the user const existing = await this.repository.findByIdEnhanced(id); if (!existing) throw new Error('Fuel log not found'); if (existing.user_id !== userId) throw new Error('Unauthorized'); // Get user settings for unit conversion const userSettings = await UserSettingsService.getUserSettings(userId); // Validate the update data if (Object.keys(data).length === 0) { throw new Error('No fields provided for update'); } // Prepare update data with proper type conversion const updateData: any = {}; if (data.dateTime !== undefined) { updateData.dateTime = new Date(data.dateTime); } if (data.odometerReading !== undefined) { updateData.odometerReading = data.odometerReading; } if (data.tripDistance !== undefined) { updateData.tripDistance = data.tripDistance; } if (data.fuelType !== undefined) { updateData.fuelType = data.fuelType; } if (data.fuelGrade !== undefined) { updateData.fuelGrade = data.fuelGrade; } if (data.fuelUnits !== undefined) { updateData.fuelUnits = data.fuelUnits; } if (data.costPerUnit !== undefined) { updateData.costPerUnit = data.costPerUnit; } if (data.locationData !== undefined) { updateData.locationData = data.locationData; } if (data.notes !== undefined) { updateData.notes = data.notes; } // Update the fuel log const updated = await this.repository.updateEnhanced(id, updateData); if (!updated) throw new Error('Failed to update fuel log'); // Update vehicle odometer if changed if (data.odometerReading !== undefined) { await pool.query( 'UPDATE vehicles SET odometer_reading = $1 WHERE id = $2 AND user_id = $3 AND (odometer_reading IS NULL OR odometer_reading < $1)', [data.odometerReading, existing.vehicle_id, userId] ); } // Invalidate caches await this.invalidateCaches(userId, existing.vehicle_id, userSettings.unitSystem); // Calculate efficiency for response const efficiency = EfficiencyCalculationService.calculateEfficiency( { odometerReading: updated.odometer ?? undefined, tripDistance: updated.trip_distance ?? undefined, fuelUnits: updated.fuel_units ?? undefined }, null, // Previous log efficiency calculation would require more complex logic for updates userSettings.unitSystem ); return this.toEnhancedResponse(updated, efficiency?.value ?? undefined, userSettings.unitSystem); } async deleteFuelLog(id: string, userId: string): Promise { const existing = await this.repository.findByIdEnhanced(id); if (!existing) throw new Error('Fuel log not found'); if (existing.user_id !== userId) throw new Error('Unauthorized'); await this.repository.delete(id); await this.invalidateCaches(userId, existing.vehicle_id, 'imperial'); // cache keys include unit; simple sweep below } async getVehicleStats(vehicleId: string, userId: string): Promise { const vehicleCheck = await pool.query('SELECT id FROM vehicles WHERE id = $1 AND user_id = $2', [vehicleId, userId]); if (vehicleCheck.rows.length === 0) throw new Error('Vehicle not found or unauthorized'); const { unitSystem } = await UserSettingsService.getUserSettings(userId); const rows = await this.repository.findByVehicleIdEnhanced(vehicleId); const labels = UnitConversionService.getUnitLabels(unitSystem); if (rows.length === 0) { return { logCount: 0, totalFuelUnits: 0, totalCost: 0, averageCostPerUnit: 0, totalDistance: 0, averageEfficiency: 0, unitLabels: labels }; } const totalFuelUnits = rows.reduce((s, r) => s + (r.fuel_units || 0), 0); const totalCost = rows.reduce((s, r) => s + (r.total_cost || 0), 0); const averageCostPerUnit = totalFuelUnits > 0 ? totalCost / totalFuelUnits : 0; const sorted = [...rows].sort((a, b) => (new Date(b.date_time || b.date)).getTime() - (new Date(a.date_time || a.date)).getTime()); let totalDistance = 0; for (let i = 0; i < sorted.length; i++) { const cur = sorted[i]; const prev = sorted[i + 1]; if (Number(cur.trip_distance) > 0) totalDistance += Number(cur.trip_distance); else if (prev && cur.odometer != null && prev.odometer != null) { const d = Number(cur.odometer) - Number(prev.odometer); if (d > 0) totalDistance += d; } } const efficiencies: number[] = sorted.map(l => { const e = EfficiencyCalculationService.calculateEfficiency( { odometerReading: l.odometer ?? undefined, tripDistance: l.trip_distance ?? undefined, fuelUnits: l.fuel_units ?? undefined }, null, unitSystem ); return e?.value || 0; }).filter(v => v > 0); const averageEfficiency = efficiencies.length ? (efficiencies.reduce((a, b) => a + b, 0) / efficiencies.length) : 0; return { logCount: rows.length, totalFuelUnits, totalCost, averageCostPerUnit, totalDistance, averageEfficiency, unitLabels: labels }; } private async invalidateCaches(userId: string, vehicleId: string, unitSystem: 'imperial' | 'metric'): Promise { await Promise.all([ cacheService.del(`${this.cachePrefix}:user:${userId}:${unitSystem}`), cacheService.del(`${this.cachePrefix}:vehicle:${vehicleId}:${unitSystem}`) ]); } private toEnhancedResponse(row: any, efficiency: number | undefined, unitSystem: 'imperial' | 'metric'): EnhancedFuelLogResponse { const labels = UnitConversionService.getUnitLabels(unitSystem); const dateTime = row.date_time ? new Date(row.date_time) : (row.date ? new Date(row.date) : new Date()); return { id: row.id, userId: row.user_id, vehicleId: row.vehicle_id, dateTime: dateTime.toISOString(), odometerReading: row.odometer ?? undefined, tripDistance: row.trip_distance ?? undefined, fuelType: row.fuel_type as FuelType, fuelGrade: row.fuel_grade ?? undefined, fuelUnits: row.fuel_units ?? 0, costPerUnit: row.cost_per_unit ?? 0, totalCost: row.total_cost ?? 0, locationData: row.location_data ?? undefined, notes: row.notes ?? undefined, efficiency: efficiency, efficiencyLabel: labels.efficiencyUnits, createdAt: new Date(row.created_at).toISOString(), updatedAt: new Date(row.updated_at).toISOString(), }; } }