Files
motovaultpro/docs/changes/database-20251111/backend-vehicles-api.md
2025-11-11 10:29:02 -06:00

25 KiB

Backend Vehicles API Update - Agent 4

Task: Update Vehicles API to use string-based parameters and responses

Status: Ready for Implementation Dependencies: Agent 3 (Platform Service) must be complete Estimated Time: 1-2 hours Assigned To: Agent 4 (Vehicles API)


Overview

Update the vehicles controller and service to accept string parameters (make, model, trim) instead of IDs, and return string arrays instead of objects. This is the final backend change before frontend updates.


Prerequisites

Verify Agent 3 Completed

# Verify service compiles
cd backend && npm run build

# Should see no errors in vehicle-data.service.ts

Files to Modify

backend/src/features/vehicles/api/vehicles.controller.ts
backend/src/features/vehicles/domain/vehicles.service.ts
backend/src/features/vehicles/types/index.ts (query parameter types)

Current Implementation Analysis

Controller

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

Current Query Parameters (ID-based):

getDropdownModels: { year: number; make_id: number }
getDropdownTrims: { year: number; make_id: number; model_id: number }
getDropdownEngines: { year: number; make_id: number; model_id: number; trim_id: number }
getDropdownTransmissions: { year: number; make_id: number; model_id: number }

Service

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

Current Methods:

getDropdownMakes(year: number): Promise<{ id: number; name: string }[]>
getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]>
getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ id: number; name: string }[]>
getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ id: number; name: string }[]>
getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ id: number; name: string }[]>  // Hardcoded!
getDropdownYears(): Promise<number[]>

Part 1: Update Vehicles Service

Step 1: Update getDropdownMakes()

Current (lines 165-171):

async getDropdownMakes(year: number): Promise<{ id: number; name: string }[]> {
  const vehicleDataService = getVehicleDataService();
  const pool = getPool();

  logger.info('Fetching dropdown makes via platform module', { year });
  return vehicleDataService.getMakes(pool, year);
}

New:

async getDropdownMakes(year: number): Promise<string[]> {
  const vehicleDataService = getVehicleDataService();
  const pool = getPool();

  logger.info('Fetching dropdown makes via platform module', { year });
  return vehicleDataService.getMakes(pool, year);
}

Changes:

  • Line 165: Return type Promise<{ id: number; name: string }[]>Promise<string[]>
  • Logic unchanged (platform service now returns string[])

Step 2: Update getDropdownModels()

Current (lines 173-179):

async getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]> {
  const vehicleDataService = getVehicleDataService();
  const pool = getPool();

  logger.info('Fetching dropdown models via platform module', { year, makeId });
  return vehicleDataService.getModels(pool, year, makeId);
}

New:

async getDropdownModels(year: number, make: string): Promise<string[]> {
  const vehicleDataService = getVehicleDataService();
  const pool = getPool();

  logger.info('Fetching dropdown models via platform module', { year, make });
  return vehicleDataService.getModels(pool, year, make);
}

Changes:

  • Line 173: Parameter makeId: numbermake: string
  • Line 173: Return type changed to Promise<string[]>
  • Line 177: Logger uses make instead of makeId
  • Line 178: Service call uses make instead of makeId

Step 3: Update getDropdownTrims()

Current (lines 197-203):

async getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ id: number; name: string }[]> {
  const vehicleDataService = getVehicleDataService();
  const pool = getPool();

  logger.info('Fetching dropdown trims via platform module', { year, makeId, modelId });
  return vehicleDataService.getTrims(pool, year, modelId);
}

New:

async getDropdownTrims(year: number, make: string, model: string): Promise<string[]> {
  const vehicleDataService = getVehicleDataService();
  const pool = getPool();

  logger.info('Fetching dropdown trims via platform module', { year, make, model });
  return vehicleDataService.getTrims(pool, year, make, model);
}

Changes:

  • Line 197: Parameters makeId: number, modelId: numbermake: string, model: string
  • Line 197: Return type changed to Promise<string[]>
  • Line 201: Logger uses make, model instead of makeId, modelId
  • Line 202: Service call uses make, model instead of modelId

Step 4: Update getDropdownEngines()

Current (lines 189-195):

async getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ id: number; name: string }[]> {
  const vehicleDataService = getVehicleDataService();
  const pool = getPool();

  logger.info('Fetching dropdown engines via platform module', { year, makeId, modelId, trimId });
  return vehicleDataService.getEngines(pool, year, modelId, trimId);
}

New:

async getDropdownEngines(year: number, make: string, model: string, trim: string): Promise<string[]> {
  const vehicleDataService = getVehicleDataService();
  const pool = getPool();

  logger.info('Fetching dropdown engines via platform module', { year, make, model, trim });
  return vehicleDataService.getEngines(pool, year, make, model, trim);
}

Changes:

  • Line 189: Parameters changed to strings: make: string, model: string, trim: string
  • Line 189: Return type changed to Promise<string[]>
  • Line 193: Logger uses string names
  • Line 194: Service call uses all string parameters

Step 5: Update getDropdownTransmissions() - CRITICAL

Current (lines 181-187):

async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ id: number; name: string }[]> {
  logger.info('Providing dropdown transmissions from static list');
  return [
    { id: 1, name: 'Automatic' },
    { id: 2, name: 'Manual' }
  ];
}

New:

async getDropdownTransmissions(year: number, make: string, model: string): Promise<string[]> {
  const vehicleDataService = getVehicleDataService();
  const pool = getPool();

  logger.info('Fetching dropdown transmissions via platform module', { year, make, model });
  return vehicleDataService.getTransmissions(pool, year, make, model);
}

Changes:

  • Line 181: Parameters changed from IDs to strings: make: string, model: string
  • Line 181: Return type changed to Promise<string[]>
  • Line 182-186: REMOVED hardcoded static list
  • Now calls platform service for real transmission data

Part 2: Update Vehicles Controller

Step 6: Update getDropdownMakes()

Current (lines 153-172):

async getDropdownMakes(request: FastifyRequest<{ Querystring: { year: number } }>, reply: FastifyReply) {
  try {
    const { year } = request.query;
    if (!year || year < 1980 || year > new Date().getFullYear() + 1) {
      return reply.code(400).send({
        error: 'Bad Request',
        message: 'Valid year parameter is required (1980-' + (new Date().getFullYear() + 1) + ')'
      });
    }

    const makes = await this.vehiclesService.getDropdownMakes(year);
    return reply.code(200).send(makes);
  } catch (error) {
    logger.error('Error getting dropdown makes', { error, year: request.query?.year });
    return reply.code(500).send({
      error: 'Internal server error',
      message: 'Failed to get makes'
    });
  }
}

New: (Unchanged - already returns array, validation unchanged)

Note: No changes needed - query parameter is already just year, and service now returns string[] which gets passed through directly.


Step 7: Update getDropdownModels()

Current (lines 174-193):

async getDropdownModels(request: FastifyRequest<{ Querystring: { year: number; make_id: number } }>, reply: FastifyReply) {
  try {
    const { year, make_id } = request.query;
    if (!year || !make_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1) {
      return reply.code(400).send({
        error: 'Bad Request',
        message: 'Valid year and make_id parameters are required'
      });
    }

    const models = await this.vehiclesService.getDropdownModels(year, make_id);
    return reply.code(200).send(models);
  } catch (error) {
    logger.error('Error getting dropdown models', { error, year: request.query?.year, make_id: request.query?.make_id });
    return reply.code(500).send({
      error: 'Internal server error',
      message: 'Failed to get models'
    });
  }
}

New:

async getDropdownModels(request: FastifyRequest<{ Querystring: { year: number; make: string } }>, reply: FastifyReply) {
  try {
    const { year, make } = request.query;
    if (!year || !make || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0) {
      return reply.code(400).send({
        error: 'Bad Request',
        message: 'Valid year and make parameters are required'
      });
    }

    const models = await this.vehiclesService.getDropdownModels(year, make);
    return reply.code(200).send(models);
  } catch (error) {
    logger.error('Error getting dropdown models', { error, year: request.query?.year, make: request.query?.make });
    return reply.code(500).send({
      error: 'Internal server error',
      message: 'Failed to get models'
    });
  }
}

Changes:

  • Line 174: Querystring type make_id: numbermake: string
  • Line 176: Destructure make instead of make_id
  • Line 177: Validation changed from make_id < 1 to make.trim().length === 0
  • Line 180: Error message updated
  • Line 184: Service call uses make string
  • Line 187: Logger uses make

Step 8: Update getDropdownTrims()

Current (lines 237-256):

async getDropdownTrims(request: FastifyRequest<{ Querystring: { year: number; make_id: number; model_id: number } }>, reply: FastifyReply) {
  try {
    const { year, make_id, model_id } = request.query;
    if (!year || !make_id || !model_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1 || model_id < 1) {
      return reply.code(400).send({
        error: 'Bad Request',
        message: 'Valid year, make_id, and model_id parameters are required'
      });
    }

    const trims = await this.vehiclesService.getDropdownTrims(year, make_id, model_id);
    return reply.code(200).send(trims);
  } catch (error) {
    logger.error('Error getting dropdown trims', { error, year: request.query?.year, make_id: request.query?.make_id, model_id: request.query?.model_id });
    return reply.code(500).send({
      error: 'Internal server error',
      message: 'Failed to get trims'
    });
  }
}

New:

async getDropdownTrims(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string } }>, reply: FastifyReply) {
  try {
    const { year, make, model } = request.query;
    if (!year || !make || !model || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0 || model.trim().length === 0) {
      return reply.code(400).send({
        error: 'Bad Request',
        message: 'Valid year, make, and model parameters are required'
      });
    }

    const trims = await this.vehiclesService.getDropdownTrims(year, make, model);
    return reply.code(200).send(trims);
  } catch (error) {
    logger.error('Error getting dropdown trims', { error, year: request.query?.year, make: request.query?.make, model: request.query?.model });
    return reply.code(500).send({
      error: 'Internal server error',
      message: 'Failed to get trims'
    });
  }
}

Changes:

  • Line 237: Querystring types changed to make: string; model: string
  • Line 239: Destructure string parameters
  • Line 240: Validation uses .trim().length === 0 instead of < 1
  • Line 243: Error message updated
  • Line 247: Service call uses strings
  • Line 250: Logger uses strings

Step 9: Update getDropdownEngines()

Current (lines 216-235):

async getDropdownEngines(request: FastifyRequest<{ Querystring: { year: number; make_id: number; model_id: number; trim_id: number } }>, reply: FastifyReply) {
  try {
    const { year, make_id, model_id, trim_id } = request.query;
    if (!year || !make_id || !model_id || !trim_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1 || model_id < 1 || trim_id < 1) {
      return reply.code(400).send({
        error: 'Bad Request',
        message: 'Valid year, make_id, model_id, and trim_id parameters are required'
      });
    }

    const engines = await this.vehiclesService.getDropdownEngines(year, make_id, model_id, trim_id);
    return reply.code(200).send(engines);
  } catch (error) {
    logger.error('Error getting dropdown engines', { error, year: request.query?.year, make_id: request.query?.make_id, model_id: request.query?.model_id, trim_id: request.query?.trim_id });
    return reply.code(500).send({
      error: 'Internal server error',
      message: 'Failed to get engines'
    });
  }
}

New:

async getDropdownEngines(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string; trim: string } }>, reply: FastifyReply) {
  try {
    const { year, make, model, trim } = request.query;
    if (!year || !make || !model || !trim || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
      return reply.code(400).send({
        error: 'Bad Request',
        message: 'Valid year, make, model, and trim parameters are required'
      });
    }

    const engines = await this.vehiclesService.getDropdownEngines(year, make, model, trim);
    return reply.code(200).send(engines);
  } catch (error) {
    logger.error('Error getting dropdown engines', { error, year: request.query?.year, make: request.query?.make, model: request.query?.model, trim: request.query?.trim });
    return reply.code(500).send({
      error: 'Internal server error',
      message: 'Failed to get engines'
    });
  }
}

Changes:

  • Line 216: Querystring types changed to strings
  • Line 218: Destructure string parameters
  • Line 219: Validation uses .trim().length === 0
  • Line 222: Error message updated
  • Line 226: Service call uses strings
  • Line 229: Logger uses strings

Step 10: Update getDropdownTransmissions()

Current (lines 195-214):

async getDropdownTransmissions(request: FastifyRequest<{ Querystring: { year: number; make_id: number; model_id: number } }>, reply: FastifyReply) {
  try {
    const { year, make_id, model_id } = request.query;
    if (!year || !make_id || !model_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1 || model_id < 1) {
      return reply.code(400).send({
        error: 'Bad Request',
        message: 'Valid year, make_id, and model_id parameters are required'
      });
    }

    const transmissions = await this.vehiclesService.getDropdownTransmissions(year, make_id, model_id);
    return reply.code(200).send(transmissions);
  } catch (error) {
    logger.error('Error getting dropdown transmissions', { error, year: request.query?.year, make_id: request.query?.make_id, model_id: request.query?.model_id });
    return reply.code(500).send({
      error: 'Internal server error',
      message: 'Failed to get transmissions'
    });
  }
}

New:

async getDropdownTransmissions(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string } }>, reply: FastifyReply) {
  try {
    const { year, make, model } = request.query;
    if (!year || !make || !model || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0 || model.trim().length === 0) {
      return reply.code(400).send({
        error: 'Bad Request',
        message: 'Valid year, make, and model parameters are required'
      });
    }

    const transmissions = await this.vehiclesService.getDropdownTransmissions(year, make, model);
    return reply.code(200).send(transmissions);
  } catch (error) {
    logger.error('Error getting dropdown transmissions', { error, year: request.query?.year, make: request.query?.make, model: request.query?.model });
    return reply.code(500).send({
      error: 'Internal server error',
      message: 'Failed to get transmissions'
    });
  }
}

Changes:

  • Line 195: Querystring types changed to strings
  • Line 197: Destructure string parameters
  • Line 198: Validation uses .trim().length === 0
  • Line 201: Error message updated
  • Line 205: Service call uses strings (now fetches real data!)
  • Line 208: Logger uses strings

Testing & Verification

Manual API Testing

Test each endpoint with the new string-based parameters:

# Assuming backend is running on localhost:3000

# Test getMakes
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=2024" \
  -H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["Acura", "Audi", "BMW", "Ford", "Honda", ...]

# Test getModels (note: make parameter, not make_id)
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=Ford" \
  -H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["Bronco", "Edge", "Escape", "Explorer", "F-150", "Mustang", ...]

# Test getTrims
curl -X GET "http://localhost:3000/api/vehicles/dropdown/trims?year=2024&make=Ford&model=F-150" \
  -H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["King Ranch", "Lariat", "Limited", "Platinum", "XL", "XLT", ...]

# Test getEngines
curl -X GET "http://localhost:3000/api/vehicles/dropdown/engines?year=2024&make=Ford&model=F-150&trim=XLT" \
  -H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["V6 2.7L Turbo", "V6 3.5L Turbo", "V8 5.0L", ...]

# Test getTransmissions (should return REAL data now, not hardcoded)
curl -X GET "http://localhost:3000/api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150" \
  -H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["10-Speed Automatic", ...] NOT ["Automatic", "Manual"]

# Test with Tesla (electric vehicle - should show N/A for engine)
curl -X GET "http://localhost:3000/api/vehicles/dropdown/engines?year=2024&make=Tesla&model=Model 3&trim=Long Range" \
  -H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["N/A (Electric)"]

Test Error Handling

# Test missing make parameter
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024" \
  -H "Authorization: Bearer YOUR_TOKEN"
# Expected: 400 Bad Request

# Test empty make parameter
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=" \
  -H "Authorization: Bearer YOUR_TOKEN"
# Expected: 400 Bad Request

# Test invalid year
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=1900" \
  -H "Authorization: Bearer YOUR_TOKEN"
# Expected: 400 Bad Request

Integration Tests

Create automated tests:

// backend/src/features/vehicles/tests/integration/dropdown-api.test.ts

describe('Dropdown API Endpoints', () => {
  let app: FastifyInstance;
  let authToken: string;

  beforeAll(async () => {
    app = await createTestApp();
    authToken = await getTestAuthToken();
  });

  describe('GET /api/vehicles/dropdown/makes', () => {
    it('should return string array of makes', async () => {
      const response = await app.inject({
        method: 'GET',
        url: '/api/vehicles/dropdown/makes?year=2024',
        headers: { Authorization: `Bearer ${authToken}` }
      });

      expect(response.statusCode).toBe(200);
      const makes = JSON.parse(response.body);
      expect(Array.isArray(makes)).toBe(true);
      expect(typeof makes[0]).toBe('string');
      expect(makes).toContain('Ford');
    });
  });

  describe('GET /api/vehicles/dropdown/models', () => {
    it('should accept make string parameter', async () => {
      const response = await app.inject({
        method: 'GET',
        url: '/api/vehicles/dropdown/models?year=2024&make=Ford',
        headers: { Authorization: `Bearer ${authToken}` }
      });

      expect(response.statusCode).toBe(200);
      const models = JSON.parse(response.body);
      expect(Array.isArray(models)).toBe(true);
      expect(typeof models[0]).toBe('string');
      expect(models).toContain('F-150');
    });

    it('should return 400 for missing make', async () => {
      const response = await app.inject({
        method: 'GET',
        url: '/api/vehicles/dropdown/models?year=2024',
        headers: { Authorization: `Bearer ${authToken}` }
      });

      expect(response.statusCode).toBe(400);
    });
  });

  describe('GET /api/vehicles/dropdown/transmissions', () => {
    it('should return real transmission data (not hardcoded)', async () => {
      const response = await app.inject({
        method: 'GET',
        url: '/api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150',
        headers: { Authorization: `Bearer ${authToken}` }
      });

      expect(response.statusCode).toBe(200);
      const transmissions = JSON.parse(response.body);
      expect(Array.isArray(transmissions)).toBe(true);
      expect(transmissions.length).toBeGreaterThan(0);

      // Should NOT be the old hardcoded values
      const hasDetailedTransmissions = transmissions.some((t: string) =>
        t.includes('Speed') || t.includes('CVT')
      );
      expect(hasDetailedTransmissions).toBe(true);
    });
  });
});

Completion Checklist

Before signaling completion:

  • Service methods updated (all use string parameters)
  • Service return types changed to string[]
  • getDropdownTransmissions() now fetches real data (not hardcoded)
  • Controller query parameter types updated (make, model, trim not IDs)
  • Controller validation updated (string length checks)
  • Controller error messages updated
  • All manual API tests pass
  • Integration tests pass
  • TypeScript compiles with no errors
  • Backend builds successfully: npm run build
  • No console errors in logs

Common Issues

Issue: "Cannot read property 'trim' of undefined"

Cause: Query parameters might be undefined if not provided

Solution:

// Add extra validation before .trim()
if (!make || typeof make !== 'string' || make.trim().length === 0) {
  return reply.code(400).send({ ... });
}

Issue: Transmissions still returning ["Automatic", "Manual"]

Cause: Service method not updated or cache not cleared

Solution:

# Clear Redis cache
docker exec mvp-redis redis-cli FLUSHDB

# Restart backend
make rebuild

Issue: Frontend still sending make_id parameter

Cause: Frontend not yet updated (Agent 5 not complete)

Solution:

  • This is expected - Agent 5 will update frontend API client
  • For now, test with curl using string parameters
  • Document breaking changes for Agent 5

API Contract for Agent 5

Once complete, provide this to Agent 5 (Frontend API Client):

New API Endpoints

All endpoints now use string parameters (not IDs):

// Makes
GET /api/vehicles/dropdown/makes?year=2024
Response: string[]  // ["Ford", "Honda", ...]

// Models
GET /api/vehicles/dropdown/models?year=2024&make=Ford
Response: string[]  // ["F-150", "Mustang", ...]

// Trims
GET /api/vehicles/dropdown/trims?year=2024&make=Ford&model=F-150
Response: string[]  // ["XLT", "Lariat", ...]

// Engines
GET /api/vehicles/dropdown/engines?year=2024&make=Ford&model=F-150&trim=XLT
Response: string[]  // ["V8 5.0L", ...]

// Transmissions
GET /api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150
Response: string[]  // ["10-Speed Automatic", ...]

// Years (unchanged)
GET /api/vehicles/dropdown/years
Response: number[]  // [2026, 2025, ...]

Breaking Changes

Query Parameters:

  • make_idmake (string)
  • model_idmodel (string)
  • trim_idtrim (string)

Response Format:

  • Old: [{id: 1, name: "Ford"}, ...]
  • New: ["Ford", ...]

Completion Message Template

Agent 4 (Vehicles API): COMPLETE

Files Modified:
- backend/src/features/vehicles/api/vehicles.controller.ts
- backend/src/features/vehicles/domain/vehicles.service.ts

Changes Made:
- Updated all controller query parameters to use strings (make, model, trim)
- Updated all service methods to accept string parameters
- Changed return types to string[] (removed objects)
- getDropdownTransmissions() now fetches real data (not hardcoded!)
- Updated validation to check string lengths
- Updated error messages to reflect new parameter names

Verification:
✓ TypeScript compiles successfully
✓ Backend builds successfully
✓ Manual API tests with curl pass
✓ Integration tests pass
✓ Transmissions return real data (verified)
✓ Electric vehicles show 'N/A (Electric)' for engines

Agent 5 (Frontend API Client) can now update frontend to use new API contract.

Breaking Changes for Agent 5:
- Query parameters changed: make_id→make, model_id→model, trim_id→trim
- Response format changed: {id, name}[] → string[]
- Cascade queries now use selected string values (not IDs)

Document Version: 1.0 Last Updated: 2025-11-10 Status: Ready for Implementation