Files
motovaultpro/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-3.md
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

29 KiB

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)

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<FuelLogResponse> {
    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<FuelLogResponse[]> {
    
    // 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<FuelLogResponse[]>(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<EnhancedFuelStats> {
    // 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

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)

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)

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)

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<FuelLog> {
    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<FuelLog | null> {
    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<FuelLog | null> {
    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<FuelLog[]> {
    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

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<FuelLogsRepository>;
  
  beforeEach(() => {
    mockRepository = new FuelLogsRepository({} as any) as jest.Mocked<FuelLogsRepository>;
    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

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