# Phase 3: API & Backend Implementation ## Overview Update API contracts, implement enhanced backend services, create new endpoints, and build comprehensive test suite for the enhanced fuel logs system. ## Prerequisites - ✅ Phase 1 completed (database schema and core types) - ✅ Phase 2 completed (enhanced business logic services) - All business logic services tested and functional ## Updated Service Layer ### Enhanced Fuel Logs Service **File**: `backend/src/features/fuel-logs/domain/fuel-logs.service.ts` (Updated) ```typescript import { FuelLogsRepository } from '../data/fuel-logs.repository'; import { FuelLog, CreateFuelLogRequest, UpdateFuelLogRequest, FuelLogResponse, FuelStats, UnitSystem } from './fuel-logs.types'; import { EnhancedValidationService } from './enhanced-validation.service'; import { EfficiencyCalculationService } from './efficiency-calculation.service'; import { UnitConversionService } from './unit-conversion.service'; import { UserSettingsService } from '../external/user-settings.service'; import { logger } from '../../../core/logging/logger'; import { cacheService } from '../../../core/config/redis'; import pool from '../../../core/config/database'; export class FuelLogsService { private readonly cachePrefix = 'fuel-logs'; private readonly cacheTTL = 300; // 5 minutes constructor(private repository: FuelLogsRepository) {} async createFuelLog(data: CreateFuelLogRequest, userId: string): Promise { logger.info('Creating enhanced fuel log', { userId, vehicleId: data.vehicleId, fuelType: data.fuelType, hasTrip: !!data.tripDistance, hasOdometer: !!data.odometerReading }); // Get user settings for unit system const userSettings = await UserSettingsService.getUserSettings(userId); // Enhanced validation const validation = EnhancedValidationService.validateFuelLogData(data, userSettings.unitSystem); if (!validation.isValid) { throw new ValidationError(`Invalid fuel log data: ${validation.errors.join(', ')}`); } // Log warnings if (validation.warnings.length > 0) { logger.warn('Fuel log validation warnings', { warnings: validation.warnings }); } // 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'); } // Calculate total cost const totalCost = data.fuelUnits * data.costPerUnit; // Get previous log for efficiency calculation const previousLog = data.odometerReading ? await this.repository.getPreviousLogByOdometer(data.vehicleId, data.odometerReading) : await this.repository.getLatestLogForVehicle(data.vehicleId); // Calculate efficiency const efficiencyResult = EfficiencyCalculationService.calculateEfficiency( { ...data, totalCost }, previousLog, userSettings.unitSystem ); // Prepare fuel log data const fuelLogData = { ...data, userId, dateTime: new Date(data.dateTime), totalCost, mpg: efficiencyResult?.value || null, efficiencyCalculationMethod: efficiencyResult?.calculationMethod || null }; // Create fuel log const fuelLog = await this.repository.create(fuelLogData); // Update vehicle odometer if provided 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] ); } // Invalidate caches await this.invalidateCaches(userId, data.vehicleId); return this.toResponse(fuelLog, userSettings.unitSystem); } async getFuelLogsByVehicle( vehicleId: string, userId: string, options?: { unitSystem?: UnitSystem } ): Promise { // Verify vehicle ownership 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'); } // Get user settings const userSettings = await UserSettingsService.getUserSettings(userId); const unitSystem = options?.unitSystem || userSettings.unitSystem; const cacheKey = `${this.cachePrefix}:vehicle:${vehicleId}:${unitSystem}`; // Check cache const cached = await cacheService.get(cacheKey); if (cached) { return cached; } // Get from database const logs = await this.repository.findByVehicleId(vehicleId); const response = logs.map((log: FuelLog) => this.toResponse(log, unitSystem)); // Cache result await cacheService.set(cacheKey, response, this.cacheTTL); return response; } async getEnhancedVehicleStats(vehicleId: string, userId: string): Promise { // Verify vehicle ownership 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 userSettings = await UserSettingsService.getUserSettings(userId); const logs = await this.repository.findByVehicleId(vehicleId); if (logs.length === 0) { return this.getEmptyStats(userSettings.unitSystem); } // Calculate comprehensive stats const totalFuelUnits = logs.reduce((sum, log) => sum + log.fuelUnits, 0); const totalCost = logs.reduce((sum, log) => sum + log.totalCost, 0); const averageCostPerUnit = totalCost / totalFuelUnits; const totalDistance = EfficiencyCalculationService.calculateTotalDistance(logs, userSettings.unitSystem); const averageEfficiency = EfficiencyCalculationService.calculateAverageEfficiency(logs, userSettings.unitSystem); // Group by fuel type const fuelTypeBreakdown = this.calculateFuelTypeBreakdown(logs, userSettings.unitSystem); // Calculate trends (last 30 days vs previous 30 days) const trends = this.calculateEfficiencyTrends(logs, userSettings.unitSystem); const unitLabels = UnitConversionService.getUnitLabels(userSettings.unitSystem); return { logCount: logs.length, totalFuelUnits, totalCost, averageCostPerUnit, totalDistance, averageEfficiency: averageEfficiency?.value || 0, fuelTypeBreakdown, trends, unitLabels, dateRange: { earliest: logs[logs.length - 1]?.dateTime, latest: logs[0]?.dateTime } }; } private toResponse(log: FuelLog, unitSystem: UnitSystem): FuelLogResponse { const unitLabels = UnitConversionService.getUnitLabels(unitSystem); // Convert efficiency to user's unit system if needed let displayEfficiency = log.mpg; if (log.mpg && unitSystem === UnitSystem.METRIC) { displayEfficiency = UnitConversionService.convertEfficiency( log.mpg, UnitSystem.IMPERIAL, // Assuming stored as MPG UnitSystem.METRIC ); } return { id: log.id, userId: log.userId, vehicleId: log.vehicleId, dateTime: log.dateTime.toISOString(), // Distance information odometerReading: log.odometerReading, tripDistance: log.tripDistance, // Fuel information fuelType: log.fuelType, fuelGrade: log.fuelGrade, fuelUnits: log.fuelUnits, costPerUnit: log.costPerUnit, totalCost: log.totalCost, // Location locationData: log.locationData, // Calculated fields efficiency: displayEfficiency, efficiencyLabel: unitLabels.efficiencyUnits, // Metadata notes: log.notes, createdAt: log.createdAt.toISOString(), updatedAt: log.updatedAt.toISOString(), // Legacy fields (for backward compatibility) date: log.dateTime.toISOString().split('T')[0], odometer: log.odometerReading, gallons: log.fuelUnits, // May need conversion pricePerGallon: log.costPerUnit, // May need conversion mpg: log.mpg }; } } ``` ### New API Endpoints #### Fuel Grade Endpoint **File**: `backend/src/features/fuel-logs/api/fuel-grade.controller.ts` ```typescript import { FastifyRequest, FastifyReply } from 'fastify'; import { FuelGradeService } from '../domain/fuel-grade.service'; import { FuelType } from '../domain/fuel-logs.types'; import { logger } from '../../../core/logging/logger'; export class FuelGradeController { async getFuelGrades( request: FastifyRequest<{ Params: { fuelType: FuelType } }>, reply: FastifyReply ) { try { const { fuelType } = request.params; // Validate fuel type if (!Object.values(FuelType).includes(fuelType)) { return reply.code(400).send({ error: 'Bad Request', message: `Invalid fuel type: ${fuelType}` }); } const grades = FuelGradeService.getFuelGradeOptions(fuelType); return reply.code(200).send({ fuelType, grades }); } catch (error: any) { logger.error('Error getting fuel grades', { error, fuelType: request.params.fuelType }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to get fuel grades' }); } } async getAllFuelTypes(request: FastifyRequest, reply: FastifyReply) { try { const fuelTypes = Object.values(FuelType).map(type => ({ value: type, label: type.charAt(0).toUpperCase() + type.slice(1), grades: FuelGradeService.getFuelGradeOptions(type) })); return reply.code(200).send({ fuelTypes }); } catch (error: any) { logger.error('Error getting fuel types', { error }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to get fuel types' }); } } } ``` ### Enhanced Routes **File**: `backend/src/features/fuel-logs/api/fuel-logs.routes.ts` (Updated) ```typescript import { FastifyInstance, FastifyPluginOptions } from 'fastify'; import { FuelLogsController } from './fuel-logs.controller'; import { FuelGradeController } from './fuel-grade.controller'; import { createFuelLogSchema, updateFuelLogSchema, fuelLogParamsSchema, vehicleParamsSchema, fuelTypeParamsSchema } from './fuel-logs.validators'; export async function fuelLogsRoutes( fastify: FastifyInstance, options: FastifyPluginOptions ) { const fuelLogsController = new FuelLogsController(); const fuelGradeController = new FuelGradeController(); // Existing fuel log CRUD endpoints (enhanced) fastify.post('/fuel-logs', { preHandler: [fastify.authenticate], schema: createFuelLogSchema }, fuelLogsController.createFuelLog.bind(fuelLogsController)); fastify.get('/fuel-logs', { preHandler: [fastify.authenticate] }, fuelLogsController.getUserFuelLogs.bind(fuelLogsController)); fastify.get('/fuel-logs/:id', { preHandler: [fastify.authenticate], schema: { params: fuelLogParamsSchema } }, fuelLogsController.getFuelLog.bind(fuelLogsController)); fastify.put('/fuel-logs/:id', { preHandler: [fastify.authenticate], schema: { params: fuelLogParamsSchema, body: updateFuelLogSchema } }, fuelLogsController.updateFuelLog.bind(fuelLogsController)); fastify.delete('/fuel-logs/:id', { preHandler: [fastify.authenticate], schema: { params: fuelLogParamsSchema } }, fuelLogsController.deleteFuelLog.bind(fuelLogsController)); // Vehicle-specific endpoints (enhanced) fastify.get('/fuel-logs/vehicle/:vehicleId', { preHandler: [fastify.authenticate], schema: { params: vehicleParamsSchema } }, fuelLogsController.getFuelLogsByVehicle.bind(fuelLogsController)); fastify.get('/fuel-logs/vehicle/:vehicleId/stats', { preHandler: [fastify.authenticate], schema: { params: vehicleParamsSchema } }, fuelLogsController.getEnhancedVehicleStats.bind(fuelLogsController)); // NEW: Fuel type/grade endpoints fastify.get('/fuel-logs/fuel-types', { preHandler: [fastify.authenticate] }, fuelGradeController.getAllFuelTypes.bind(fuelGradeController)); fastify.get('/fuel-logs/fuel-grades/:fuelType', { preHandler: [fastify.authenticate], schema: { params: fuelTypeParamsSchema } }, fuelGradeController.getFuelGrades.bind(fuelGradeController)); } export function registerFuelLogsRoutes(fastify: FastifyInstance) { return fastify.register(fuelLogsRoutes, { prefix: '/api' }); } ``` ### Enhanced Validation Schemas **File**: `backend/src/features/fuel-logs/api/fuel-logs.validators.ts` (Updated) ```typescript import { Type } from '@sinclair/typebox'; import { FuelType } from '../domain/fuel-logs.types'; export const createFuelLogSchema = { body: Type.Object({ vehicleId: Type.String({ format: 'uuid' }), dateTime: Type.String({ format: 'date-time' }), // Distance (one required) odometerReading: Type.Optional(Type.Number({ minimum: 0 })), tripDistance: Type.Optional(Type.Number({ minimum: 0 })), // Fuel system fuelType: Type.Enum(FuelType), fuelGrade: Type.Optional(Type.String()), fuelUnits: Type.Number({ minimum: 0.01 }), costPerUnit: Type.Number({ minimum: 0.01 }), // Location (optional) locationData: Type.Optional(Type.Object({ address: Type.Optional(Type.String()), coordinates: Type.Optional(Type.Object({ latitude: Type.Number({ minimum: -90, maximum: 90 }), longitude: Type.Number({ minimum: -180, maximum: 180 }) })), googlePlaceId: Type.Optional(Type.String()), stationName: Type.Optional(Type.String()) })), notes: Type.Optional(Type.String({ maxLength: 500 })) }), response: { 201: Type.Object({ id: Type.String({ format: 'uuid' }), userId: Type.String(), vehicleId: Type.String({ format: 'uuid' }), dateTime: Type.String({ format: 'date-time' }), odometerReading: Type.Optional(Type.Number()), tripDistance: Type.Optional(Type.Number()), fuelType: Type.Enum(FuelType), fuelGrade: Type.Optional(Type.String()), fuelUnits: Type.Number(), costPerUnit: Type.Number(), totalCost: Type.Number(), efficiency: Type.Optional(Type.Number()), efficiencyLabel: Type.String(), createdAt: Type.String({ format: 'date-time' }), updatedAt: Type.String({ format: 'date-time' }) }) } }; export const updateFuelLogSchema = { body: Type.Partial(Type.Object({ dateTime: Type.String({ format: 'date-time' }), odometerReading: Type.Number({ minimum: 0 }), tripDistance: Type.Number({ minimum: 0 }), fuelType: Type.Enum(FuelType), fuelGrade: Type.String(), fuelUnits: Type.Number({ minimum: 0.01 }), costPerUnit: Type.Number({ minimum: 0.01 }), locationData: Type.Object({ address: Type.Optional(Type.String()), coordinates: Type.Optional(Type.Object({ latitude: Type.Number({ minimum: -90, maximum: 90 }), longitude: Type.Number({ minimum: -180, maximum: 180 }) })), googlePlaceId: Type.Optional(Type.String()), stationName: Type.Optional(Type.String()) }), notes: Type.String({ maxLength: 500 }) })) }; export const fuelLogParamsSchema = Type.Object({ id: Type.String({ format: 'uuid' }) }); export const vehicleParamsSchema = Type.Object({ vehicleId: Type.String({ format: 'uuid' }) }); export const fuelTypeParamsSchema = Type.Object({ fuelType: Type.Enum(FuelType) }); ``` ## Repository Layer Updates ### Enhanced Repository **File**: `backend/src/features/fuel-logs/data/fuel-logs.repository.ts` (Updated) ```typescript import { Pool } from 'pg'; import { FuelLog, CreateFuelLogData } from '../domain/fuel-logs.types'; export interface CreateFuelLogData { userId: string; vehicleId: string; dateTime: Date; odometerReading?: number; tripDistance?: number; fuelType: string; fuelGrade?: string; fuelUnits: number; costPerUnit: number; totalCost: number; locationData?: any; notes?: string; mpg?: number; efficiencyCalculationMethod?: string; } export class FuelLogsRepository { constructor(private pool: Pool) {} async create(data: CreateFuelLogData): Promise { const query = ` INSERT INTO fuel_logs ( user_id, vehicle_id, date_time, odometer_reading, trip_distance, fuel_type, fuel_grade, fuel_units, cost_per_unit, total_cost, location_data, notes, mpg, efficiency_calculation_method, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, NOW(), NOW() ) RETURNING * `; const values = [ data.userId, data.vehicleId, data.dateTime, data.odometerReading || null, data.tripDistance || null, data.fuelType, data.fuelGrade || null, data.fuelUnits, data.costPerUnit, data.totalCost, data.locationData ? JSON.stringify(data.locationData) : null, data.notes || null, data.mpg || null, data.efficiencyCalculationMethod || null ]; const result = await this.pool.query(query, values); return this.mapRowToFuelLog(result.rows[0]); } async getPreviousLogByOdometer(vehicleId: string, currentOdometer: number): Promise { const query = ` SELECT * FROM fuel_logs WHERE vehicle_id = $1 AND odometer_reading IS NOT NULL AND odometer_reading < $2 ORDER BY odometer_reading DESC, date_time DESC LIMIT 1 `; const result = await this.pool.query(query, [vehicleId, currentOdometer]); return result.rows.length > 0 ? this.mapRowToFuelLog(result.rows[0]) : null; } async getLatestLogForVehicle(vehicleId: string): Promise { const query = ` SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC, created_at DESC LIMIT 1 `; const result = await this.pool.query(query, [vehicleId]); return result.rows.length > 0 ? this.mapRowToFuelLog(result.rows[0]) : null; } async findByVehicleId(vehicleId: string): Promise { const query = ` SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC, created_at DESC `; const result = await this.pool.query(query, [vehicleId]); return result.rows.map(row => this.mapRowToFuelLog(row)); } private mapRowToFuelLog(row: any): FuelLog { return { id: row.id, userId: row.user_id, vehicleId: row.vehicle_id, dateTime: row.date_time, odometerReading: row.odometer_reading, tripDistance: row.trip_distance, fuelType: row.fuel_type, fuelGrade: row.fuel_grade, fuelUnits: parseFloat(row.fuel_units), costPerUnit: parseFloat(row.cost_per_unit), totalCost: parseFloat(row.total_cost), locationData: row.location_data ? JSON.parse(row.location_data) : null, notes: row.notes, mpg: row.mpg ? parseFloat(row.mpg) : null, createdAt: row.created_at, updatedAt: row.updated_at, // Legacy field mapping date: row.date_time, odometer: row.odometer_reading, gallons: parseFloat(row.fuel_units), // Assuming stored in user's preferred units pricePerGallon: parseFloat(row.cost_per_unit) }; } } ``` ## Comprehensive Test Suite ### Service Layer Tests **File**: `backend/src/features/fuel-logs/tests/unit/enhanced-fuel-logs.service.test.ts` ```typescript import { FuelLogsService } from '../../domain/fuel-logs.service'; import { FuelLogsRepository } from '../../data/fuel-logs.repository'; import { FuelType, UnitSystem } from '../../domain/fuel-logs.types'; import { UserSettingsService } from '../../external/user-settings.service'; // Mock dependencies jest.mock('../../data/fuel-logs.repository'); jest.mock('../../external/user-settings.service'); jest.mock('../../../core/config/database'); jest.mock('../../../core/config/redis'); describe('Enhanced FuelLogsService', () => { let service: FuelLogsService; let mockRepository: jest.Mocked; beforeEach(() => { mockRepository = new FuelLogsRepository({} as any) as jest.Mocked; service = new FuelLogsService(mockRepository); // Mock user settings (UserSettingsService.getUserSettings as jest.Mock).mockResolvedValue({ unitSystem: UnitSystem.IMPERIAL, currencyCode: 'USD', timeZone: 'America/New_York' }); }); describe('createFuelLog', () => { it('should create fuel log with trip distance', async () => { const createData = { vehicleId: 'vehicle-id', dateTime: '2024-01-15T10:30:00Z', tripDistance: 300, fuelType: FuelType.GASOLINE, fuelGrade: '87', fuelUnits: 10, costPerUnit: 3.50, notes: 'Test fuel log' }; // Mock vehicle check (pool.query as jest.Mock) .mockResolvedValueOnce({ rows: [{ id: 'vehicle-id' }] }) // Vehicle exists .mockResolvedValueOnce({}); // Odometer update (not applicable for trip distance) mockRepository.create.mockResolvedValue({ id: 'fuel-log-id', userId: 'user-id', ...createData, totalCost: 35.0, mpg: 30, createdAt: new Date(), updatedAt: new Date() } as any); const result = await service.createFuelLog(createData, 'user-id'); expect(result.id).toBe('fuel-log-id'); expect(result.totalCost).toBe(35.0); expect(result.efficiency).toBe(30); expect(mockRepository.create).toHaveBeenCalledWith( expect.objectContaining({ tripDistance: 300, totalCost: 35.0 }) ); }); it('should validate distance requirement', async () => { const createData = { vehicleId: 'vehicle-id', dateTime: '2024-01-15T10:30:00Z', fuelType: FuelType.GASOLINE, fuelGrade: '87', fuelUnits: 10, costPerUnit: 3.50 // Missing both tripDistance and odometerReading }; await expect(service.createFuelLog(createData, 'user-id')) .rejects.toThrow('Either odometer reading or trip distance is required'); }); it('should validate fuel grade for fuel type', async () => { const createData = { vehicleId: 'vehicle-id', dateTime: '2024-01-15T10:30:00Z', tripDistance: 300, fuelType: FuelType.GASOLINE, fuelGrade: '#1', // Invalid for gasoline fuelUnits: 10, costPerUnit: 3.50 }; await expect(service.createFuelLog(createData, 'user-id')) .rejects.toThrow('Invalid fuel grade'); }); }); describe('getEnhancedVehicleStats', () => { it('should calculate comprehensive vehicle statistics', async () => { const mockLogs = [ { fuelUnits: 10, totalCost: 35, tripDistance: 300, mpg: 30, fuelType: FuelType.GASOLINE, dateTime: new Date('2024-01-15') }, { fuelUnits: 12, totalCost: 42, tripDistance: 350, mpg: 29, fuelType: FuelType.GASOLINE, dateTime: new Date('2024-01-10') } ]; // Mock vehicle check (pool.query as jest.Mock).mockResolvedValue({ rows: [{ id: 'vehicle-id' }] }); mockRepository.findByVehicleId.mockResolvedValue(mockLogs as any); const stats = await service.getEnhancedVehicleStats('vehicle-id', 'user-id'); expect(stats.logCount).toBe(2); expect(stats.totalFuelUnits).toBe(22); expect(stats.totalCost).toBe(77); expect(stats.averageCostPerUnit).toBeCloseTo(3.5, 2); expect(stats.totalDistance).toBe(650); expect(stats.averageEfficiency).toBeCloseTo(29.5, 1); }); }); }); ``` ### Integration Tests **File**: `backend/src/features/fuel-logs/tests/integration/enhanced-fuel-logs.integration.test.ts` ```typescript import request from 'supertest'; import { app } from '../../../app'; import { pool } from '../../../core/config/database'; import { FuelType } from '../../domain/fuel-logs.types'; describe('Enhanced Fuel Logs API Integration', () => { let authToken: string; let vehicleId: string; beforeAll(async () => { // Setup test data authToken = await getTestAuthToken(); vehicleId = await createTestVehicle(); }); afterAll(async () => { // Cleanup await cleanupTestData(); await pool.end(); }); describe('POST /api/fuel-logs', () => { it('should create fuel log with enhanced fields', async () => { const fuelLogData = { vehicleId, dateTime: '2024-01-15T10:30:00Z', tripDistance: 300, fuelType: FuelType.GASOLINE, fuelGrade: '87', fuelUnits: 10, costPerUnit: 3.50, locationData: { address: '123 Main St, Anytown, USA', stationName: 'Shell Station' }, notes: 'Full tank' }; const response = await request(app) .post('/api/fuel-logs') .set('Authorization', `Bearer ${authToken}`) .send(fuelLogData) .expect(201); expect(response.body.id).toBeDefined(); expect(response.body.tripDistance).toBe(300); expect(response.body.fuelType).toBe(FuelType.GASOLINE); expect(response.body.fuelGrade).toBe('87'); expect(response.body.totalCost).toBe(35.0); expect(response.body.efficiency).toBe(30); // 300 miles / 10 gallons expect(response.body.efficiencyLabel).toBe('mpg'); }); it('should validate distance requirement', async () => { const fuelLogData = { vehicleId, dateTime: '2024-01-15T10:30:00Z', fuelType: FuelType.GASOLINE, fuelGrade: '87', fuelUnits: 10, costPerUnit: 3.50 // Missing both tripDistance and odometerReading }; const response = await request(app) .post('/api/fuel-logs') .set('Authorization', `Bearer ${authToken}`) .send(fuelLogData) .expect(400); expect(response.body.message).toContain('Either odometer reading or trip distance is required'); }); }); describe('GET /api/fuel-logs/fuel-grades/:fuelType', () => { it('should return gasoline fuel grades', async () => { const response = await request(app) .get('/api/fuel-logs/fuel-grades/gasoline') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(response.body.fuelType).toBe('gasoline'); expect(response.body.grades).toHaveLength(5); expect(response.body.grades[0]).toEqual({ value: '87', label: '87 (Regular)', description: 'Regular unleaded gasoline' }); }); it('should return empty grades for electric', async () => { const response = await request(app) .get('/api/fuel-logs/fuel-grades/electric') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(response.body.fuelType).toBe('electric'); expect(response.body.grades).toHaveLength(0); }); }); describe('GET /api/fuel-logs/fuel-types', () => { it('should return all fuel types with grades', async () => { const response = await request(app) .get('/api/fuel-logs/fuel-types') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(response.body.fuelTypes).toHaveLength(3); const gasoline = response.body.fuelTypes.find(ft => ft.value === 'gasoline'); expect(gasoline.grades).toHaveLength(5); const electric = response.body.fuelTypes.find(ft => ft.value === 'electric'); expect(electric.grades).toHaveLength(0); }); }); }); ``` ## Implementation Tasks ### Service Layer Updates 1. ✅ Update FuelLogsService with enhanced business logic 2. ✅ Integrate validation and efficiency calculation services 3. ✅ Add user settings integration 4. ✅ Implement comprehensive stats calculations ### API Layer Updates 1. ✅ Create FuelGradeController for dynamic grades 2. ✅ Update existing controllers with enhanced validation 3. ✅ Add new API endpoints for fuel types/grades 4. ✅ Update validation schemas ### Repository Updates 1. ✅ Update repository for new database fields 2. ✅ Add methods for enhanced queries 3. ✅ Implement proper data mapping ### Testing Implementation 1. ✅ Create comprehensive unit test suite 2. ✅ Implement integration tests for all endpoints 3. ✅ Add validation testing 4. ✅ Test business logic edge cases ## Success Criteria ### Phase 3 Complete When: - ✅ All API endpoints functional with enhanced data - ✅ Comprehensive validation working correctly - ✅ Fuel type/grade system fully operational - ✅ Unit conversion integration functional - ✅ Enhanced statistics calculations working - ✅ Complete test suite passes (>90% coverage) - ✅ All new endpoints documented and tested - ✅ Backward compatibility maintained ### Ready for Phase 4 When: - All backend services tested and stable - API contracts finalized and documented - Frontend integration points clearly defined - Enhanced business logic fully functional --- **Next Phase**: [Phase 4 - Frontend Implementation](FUEL-LOGS-PHASE-4.md)