Files
motovaultpro/docs/changes/vehicles-dropdown-v1/phase-03-api-migration.md
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

12 KiB

Phase 3: API Migration

Overview

This phase updates the vehicles API controller to use the new MVP Platform database for all dropdown endpoints while maintaining exact API compatibility. All existing response formats and authentication patterns are preserved.

Prerequisites

  • Phase 2 backend migration completed successfully
  • VIN decoder service functional
  • MVP Platform repository working correctly
  • Backend service can query MVP Platform database
  • All TypeScript compilation successful

Current API Endpoints to Update

Existing endpoints that will be updated:

  • GET /api/vehicles/dropdown/makes (unauthenticated)
  • GET /api/vehicles/dropdown/models/:make (unauthenticated)
  • GET /api/vehicles/dropdown/transmissions (unauthenticated)
  • GET /api/vehicles/dropdown/engines (unauthenticated)
  • GET /api/vehicles/dropdown/trims (unauthenticated)

Existing endpoints that remain unchanged:

  • POST /api/vehicles (authenticated - uses VIN decoder)
  • GET /api/vehicles (authenticated)
  • GET /api/vehicles/:id (authenticated)
  • PUT /api/vehicles/:id (authenticated)
  • DELETE /api/vehicles/:id (authenticated)

Tasks

Task 3.1: Update Vehicles Controller

Location: backend/src/features/vehicles/api/vehicles.controller.ts

Action: Replace external API dropdown methods with MVP Platform database calls:

// UPDATE imports - REMOVE:
// import { vpicClient } from '../external/vpic/vpic.client';

// ADD new imports:
import { VehiclesService } from '../domain/vehicles.service';

export class VehiclesController {
  private vehiclesService: VehiclesService;

  constructor() {
    this.vehiclesService = new VehiclesService();
  }

  // UPDATE existing dropdown methods:

  async getDropdownMakes(request: FastifyRequest, reply: FastifyReply) {
    try {
      logger.info('Getting dropdown makes from MVP Platform');
      const makes = await this.vehiclesService.getDropdownMakes();
      
      // Maintain exact same response format
      const response = makes.map(make => ({
        Make_ID: make.id,
        Make_Name: make.name
      }));

      reply.status(200).send(response);
    } catch (error) {
      logger.error('Get dropdown makes failed', { error });
      reply.status(500).send({ error: 'Failed to retrieve makes' });
    }
  }

  async getDropdownModels(request: FastifyRequest<{ Params: { make: string } }>, reply: FastifyReply) {
    try {
      const { make } = request.params;
      logger.info('Getting dropdown models from MVP Platform', { make });
      
      const models = await this.vehiclesService.getDropdownModels(make);
      
      // Maintain exact same response format
      const response = models.map(model => ({
        Model_ID: model.id,
        Model_Name: model.name
      }));

      reply.status(200).send(response);
    } catch (error) {
      logger.error('Get dropdown models failed', { error });
      reply.status(500).send({ error: 'Failed to retrieve models' });
    }
  }

  async getDropdownTransmissions(request: FastifyRequest, reply: FastifyReply) {
    try {
      logger.info('Getting dropdown transmissions from MVP Platform');
      const transmissions = await this.vehiclesService.getDropdownTransmissions();
      
      // Maintain exact same response format
      const response = transmissions.map(transmission => ({
        Name: transmission.name
      }));

      reply.status(200).send(response);
    } catch (error) {
      logger.error('Get dropdown transmissions failed', { error });
      reply.status(500).send({ error: 'Failed to retrieve transmissions' });
    }
  }

  async getDropdownEngines(request: FastifyRequest, reply: FastifyReply) {
    try {
      logger.info('Getting dropdown engines from MVP Platform');
      const engines = await this.vehiclesService.getDropdownEngines();
      
      // Maintain exact same response format
      const response = engines.map(engine => ({
        Name: engine.name
      }));

      reply.status(200).send(response);
    } catch (error) {
      logger.error('Get dropdown engines failed', { error });
      reply.status(500).send({ error: 'Failed to retrieve engines' });
    }
  }

  async getDropdownTrims(request: FastifyRequest, reply: FastifyReply) {
    try {
      logger.info('Getting dropdown trims from MVP Platform');
      const trims = await this.vehiclesService.getDropdownTrims();
      
      // Maintain exact same response format  
      const response = trims.map(trim => ({
        Name: trim.name
      }));

      reply.status(200).send(response);
    } catch (error) {
      logger.error('Get dropdown trims failed', { error });
      reply.status(500).send({ error: 'Failed to retrieve trims' });
    }
  }

  // All other methods remain unchanged (createVehicle, getUserVehicles, etc.)
}

Task 3.2: Verify Routes Configuration

Location: backend/src/features/vehicles/api/vehicles.routes.ts

Action: Ensure dropdown routes remain unauthenticated (no changes needed, just verification):

// VERIFY these routes remain unauthenticated:
fastify.get('/vehicles/dropdown/makes', {
  handler: vehiclesController.getDropdownMakes.bind(vehiclesController)
});

fastify.get<{ Params: { make: string } }>('/vehicles/dropdown/models/:make', {
  handler: vehiclesController.getDropdownModels.bind(vehiclesController)
});

fastify.get('/vehicles/dropdown/transmissions', {
  handler: vehiclesController.getDropdownTransmissions.bind(vehiclesController)
});

fastify.get('/vehicles/dropdown/engines', {
  handler: vehiclesController.getDropdownEngines.bind(vehiclesController)
});

fastify.get('/vehicles/dropdown/trims', {
  handler: vehiclesController.getDropdownTrims.bind(vehiclesController)
});

Note: These routes should NOT have preHandler: fastify.authenticate to maintain unauthenticated access as required by security.md.

Task 3.3: Update Response Error Handling

Action: Add specific error handling for database connectivity issues:

// Add to VehiclesController class:

private handleDatabaseError(error: any, operation: string, reply: FastifyReply) {
  logger.error(`${operation} database error`, { error });
  
  // Check for specific database connection errors
  if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
    reply.status(503).send({ 
      error: 'Service temporarily unavailable',
      message: 'Database connection issue'
    });
    return;
  }
  
  // Generic database error
  if (error.code && error.code.startsWith('P')) { // PostgreSQL error codes
    reply.status(500).send({
      error: 'Database query failed',
      message: 'Please try again later'
    });
    return;
  }
  
  // Generic error
  reply.status(500).send({
    error: `Failed to ${operation}`,
    message: 'Internal server error'
  });
}

// Update all dropdown methods to use this error handler:
// Replace each catch block with:
} catch (error) {
  this.handleDatabaseError(error, 'retrieve makes', reply);
}

Task 3.4: Add Performance Monitoring

Action: Add response time logging for performance monitoring:

// Add to VehiclesController class:

private async measurePerformance<T>(
  operation: string,
  fn: () => Promise<T>
): Promise<T> {
  const startTime = Date.now();
  try {
    const result = await fn();
    const duration = Date.now() - startTime;
    logger.info(`MVP Platform ${operation} completed`, { duration });
    return result;
  } catch (error) {
    const duration = Date.now() - startTime;
    logger.error(`MVP Platform ${operation} failed`, { duration, error });
    throw error;
  }
}

// Update dropdown methods to use performance monitoring:
async getDropdownMakes(request: FastifyRequest, reply: FastifyReply) {
  try {
    logger.info('Getting dropdown makes from MVP Platform');
    const makes = await this.measurePerformance('makes query', () =>
      this.vehiclesService.getDropdownMakes()
    );
    
    // ... rest of method unchanged
  } catch (error) {
    this.handleDatabaseError(error, 'retrieve makes', reply);
  }
}

Task 3.5: Update Health Check

Location: backend/src/features/vehicles/api/vehicles.controller.ts

Action: Add MVP Platform database health check method:

// Add new health check method:
async healthCheck(request: FastifyRequest, reply: FastifyReply) {
  try {
    // Test MVP Platform database connection
    await this.measurePerformance('health check', async () => {
      const testResult = await this.vehiclesService.testMvpPlatformConnection();
      if (!testResult) {
        throw new Error('MVP Platform database connection failed');
      }
    });
    
    reply.status(200).send({ 
      status: 'healthy',
      mvpPlatform: 'connected',
      timestamp: new Date().toISOString()
    });
  } catch (error) {
    logger.error('Health check failed', { error });
    reply.status(503).send({
      status: 'unhealthy', 
      error: error.message,
      timestamp: new Date().toISOString()
    });
  }
}

Location: backend/src/features/vehicles/domain/vehicles.service.ts

Action: Add health check method to service:

// Add to VehiclesService class:
async testMvpPlatformConnection(): Promise<boolean> {
  try {
    await mvpPlatformRepository.getMakes();
    return true;
  } catch (error) {
    logger.error('MVP Platform connection test failed', { error });
    return false;
  }
}

Task 3.6: Update Route Registration for Health Check

Location: backend/src/features/vehicles/api/vehicles.routes.ts

Action: Add health check route:

// Add health check route (unauthenticated for monitoring):
fastify.get('/vehicles/health', {
  handler: vehiclesController.healthCheck.bind(vehiclesController)
});

Validation Steps

Step 1: Test API Response Formats

# Test makes endpoint
curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '.[0]'
# Should return: {"Make_ID": number, "Make_Name": "string"}

# Test models endpoint  
curl -s "http://localhost:3001/api/vehicles/dropdown/models/Honda" | jq '.[0]'
# Should return: {"Model_ID": number, "Model_Name": "string"}

# Test transmissions endpoint
curl -s http://localhost:3001/api/vehicles/dropdown/transmissions | jq '.[0]'
# Should return: {"Name": "string"}

Step 2: Test Performance

# Test response times (should be < 100ms)
time curl -s http://localhost:3001/api/vehicles/dropdown/makes > /dev/null

# Load test with multiple concurrent requests
for i in {1..10}; do
  curl -s http://localhost:3001/api/vehicles/dropdown/makes > /dev/null &
done
wait

Step 3: Test Error Handling

# Test with invalid make name
curl -s "http://localhost:3001/api/vehicles/dropdown/models/InvalidMake" | jq '.'
# Should return empty array or appropriate error

# Test health check
curl -s http://localhost:3001/api/vehicles/health | jq '.'
# Should return: {"status": "healthy", "mvpPlatform": "connected", "timestamp": "..."}

Step 4: Verify Authentication Patterns

# Test that dropdown endpoints are unauthenticated (should work without token)
curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '. | length'
# Should return number > 0

# Test that vehicle CRUD endpoints still require authentication  
curl -s http://localhost:3001/api/vehicles
# Should return 401 Unauthorized

Error Handling

Common Issues and Solutions

Issue: Empty response arrays Solution: Check MVP Platform database has data, verify SQL queries, check table names

Issue: Slow response times (> 100ms) Solution: Add database indexes, optimize queries, check connection pool settings

Issue: Authentication errors on dropdown endpoints Solution: Verify routes don't have authentication middleware, check security.md compliance

Issue: Wrong response format Solution: Compare with original vPIC API responses, adjust mapping in controller

Rollback Procedure

  1. Revert vehicles.controller.ts:

    git checkout HEAD -- backend/src/features/vehicles/api/vehicles.controller.ts
    
  2. Revert vehicles.routes.ts if modified:

    git checkout HEAD -- backend/src/features/vehicles/api/vehicles.routes.ts
    
  3. Restart backend service:

    docker-compose restart backend
    

Next Steps

After successful completion of Phase 3:

  1. Proceed to Phase 4: Scheduled ETL
  2. Monitor API response times in production
  3. Set up alerts for health check failures

Dependencies for Next Phase

  • All dropdown APIs returning correct data
  • Response times consistently under 100ms
  • Health check endpoint functional
  • No authentication issues with dropdown endpoints
  • Error handling working properly