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

22 KiB

Backend Platform Service Update - Agent 3

Task: Update VehicleDataService to use string-based signatures

Status: Ready for Implementation Dependencies: Agent 2 (Platform Repository) must be complete Estimated Time: 1 hour Assigned To: Agent 3 (Platform Service)


Overview

Update the service layer to match the new repository signatures. Change from ID-based parameters to string-based parameters, and update return types from objects to string arrays.


Prerequisites

Verify Agent 2 Completed

# Verify repository compiles
cd backend && npm run build

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

Files to Modify

backend/src/features/platform/domain/vehicle-data.service.ts
backend/src/features/platform/domain/platform-cache.service.ts (cache keys)
backend/src/features/platform/models/responses.ts (if not already updated)

Current Implementation Analysis

File: backend/src/features/platform/domain/vehicle-data.service.ts

Current Methods:

getYears(pool: Pool): Promise<number[]>
getMakes(pool: Pool, year: number): Promise<MakeItem[]>
getModels(pool: Pool, year: number, makeId: number): Promise<ModelItem[]>
getTrims(pool: Pool, year: number, modelId: number): Promise<TrimItem[]>
getEngines(pool: Pool, year: number, modelId: number, trimId: number): Promise<EngineItem[]>

Pattern: Each method has:

  1. Cache lookup
  2. Repository call if cache miss
  3. Cache storage
  4. Logging

Step 1: Update Imports

Current (line 8):

import { MakeItem, ModelItem, TrimItem, EngineItem } from '../models/responses';

New:

import { VINDecodeResult } from '../models/responses';
// MakeItem, ModelItem, TrimItem, EngineItem removed - using string[] now

Step 2: Update getMakes() Method

Current (lines 44-60):

async getMakes(pool: Pool, year: number): Promise<MakeItem[]> {
  try {
    const cached = await this.cache.getMakes(year);
    if (cached) {
      logger.debug('Makes retrieved from cache', { year });
      return cached;
    }

    const makes = await this.repository.getMakes(pool, year);
    await this.cache.setMakes(year, makes);
    logger.debug('Makes retrieved from database and cached', { year, count: makes.length });
    return makes;
  } catch (error) {
    logger.error('Service error: getMakes', { error, year });
    throw error;
  }
}

New:

async getMakes(pool: Pool, year: number): Promise<string[]> {
  try {
    const cached = await this.cache.getMakes(year);
    if (cached) {
      logger.debug('Makes retrieved from cache', { year });
      return cached;
    }

    const makes = await this.repository.getMakes(pool, year);
    await this.cache.setMakes(year, makes);
    logger.debug('Makes retrieved from database and cached', { year, count: makes.length });
    return makes;
  } catch (error) {
    logger.error('Service error: getMakes', { error, year });
    throw error;
  }
}

Changes:

  • Line 44: Return type Promise<MakeItem[]>Promise<string[]>
  • Logic unchanged (cache still works the same way)

Note: Cache service will need updates (Step 7)


Step 3: Update getModels() Method

Current (lines 65-81):

async getModels(pool: Pool, year: number, makeId: number): Promise<ModelItem[]> {
  try {
    const cached = await this.cache.getModels(year, makeId);
    if (cached) {
      logger.debug('Models retrieved from cache', { year, makeId });
      return cached;
    }

    const models = await this.repository.getModels(pool, year, makeId);
    await this.cache.setModels(year, makeId, models);
    logger.debug('Models retrieved from database and cached', { year, makeId, count: models.length });
    return models;
  } catch (error) {
    logger.error('Service error: getModels', { error, year, makeId });
    throw error;
  }
}

New:

async getModels(pool: Pool, year: number, make: string): Promise<string[]> {
  try {
    const cached = await this.cache.getModels(year, make);
    if (cached) {
      logger.debug('Models retrieved from cache', { year, make });
      return cached;
    }

    const models = await this.repository.getModels(pool, year, make);
    await this.cache.setModels(year, make, models);
    logger.debug('Models retrieved from database and cached', { year, make, count: models.length });
    return models;
  } catch (error) {
    logger.error('Service error: getModels', { error, year, make });
    throw error;
  }
}

Changes:

  • Line 65: Parameter makeId: numbermake: string
  • Line 65: Return type Promise<ModelItem[]>Promise<string[]>
  • Line 67: Cache call uses make instead of makeId
  • Line 69: Logger uses make instead of makeId
  • Line 73: Repository call uses make instead of makeId
  • Line 74: Cache set uses make instead of makeId
  • Line 75: Logger uses make instead of makeId
  • Line 78: Logger uses make instead of makeId

Step 4: Update getTrims() Method

Current (lines 86-102):

async getTrims(pool: Pool, year: number, modelId: number): Promise<TrimItem[]> {
  try {
    const cached = await this.cache.getTrims(year, modelId);
    if (cached) {
      logger.debug('Trims retrieved from cache', { year, modelId });
      return cached;
    }

    const trims = await this.repository.getTrims(pool, year, modelId);
    await this.cache.setTrims(year, modelId, trims);
    logger.debug('Trims retrieved from database and cached', { year, modelId, count: trims.length });
    return trims;
  } catch (error) {
    logger.error('Service error: getTrims', { error, year, modelId });
    throw error;
  }
}

New:

async getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
  try {
    const cached = await this.cache.getTrims(year, make, model);
    if (cached) {
      logger.debug('Trims retrieved from cache', { year, make, model });
      return cached;
    }

    const trims = await this.repository.getTrims(pool, year, make, model);
    await this.cache.setTrims(year, make, model, trims);
    logger.debug('Trims retrieved from database and cached', { year, make, model, count: trims.length });
    return trims;
  } catch (error) {
    logger.error('Service error: getTrims', { error, year, make, model });
    throw error;
  }
}

Changes:

  • Line 86: Parameters changed from modelId: number to make: string, model: string
  • Line 86: Return type Promise<TrimItem[]>Promise<string[]>
  • All cache and logger calls updated to use make, model instead of modelId

Step 5: Update getEngines() Method

Current (lines 107-123):

async getEngines(pool: Pool, year: number, modelId: number, trimId: number): Promise<EngineItem[]> {
  try {
    const cached = await this.cache.getEngines(year, modelId, trimId);
    if (cached) {
      logger.debug('Engines retrieved from cache', { year, modelId, trimId });
      return cached;
    }

    const engines = await this.repository.getEngines(pool, year, modelId, trimId);
    await this.cache.setEngines(year, modelId, trimId, engines);
    logger.debug('Engines retrieved from database and cached', { year, modelId, trimId, count: engines.length });
    return engines;
  } catch (error) {
    logger.error('Service error: getEngines', { error, year, modelId, trimId });
    throw error;
  }
}

New:

async getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]> {
  try {
    const cached = await this.cache.getEngines(year, make, model, trim);
    if (cached) {
      logger.debug('Engines retrieved from cache', { year, make, model, trim });
      return cached;
    }

    const engines = await this.repository.getEngines(pool, year, make, model, trim);
    await this.cache.setEngines(year, make, model, trim, engines);
    logger.debug('Engines retrieved from database and cached', { year, make, model, trim, count: engines.length });
    return engines;
  } catch (error) {
    logger.error('Service error: getEngines', { error, year, make, model, trim });
    throw error;
  }
}

Changes:

  • Line 107: Parameters changed from modelId: number, trimId: number to make: string, model: string, trim: string
  • Line 107: Return type Promise<EngineItem[]>Promise<string[]>
  • All cache and logger calls updated to use make, model, trim instead of modelId, trimId

Step 6: Add getTransmissions() Method (NEW)

Add this new method after getEngines():

/**
 * Get transmissions for a year, make, and model with caching
 */
async getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
  try {
    const cached = await this.cache.getTransmissions(year, make, model);
    if (cached) {
      logger.debug('Transmissions retrieved from cache', { year, make, model });
      return cached;
    }

    const transmissions = await this.repository.getTransmissions(pool, year, make, model);
    await this.cache.setTransmissions(year, make, model, transmissions);
    logger.debug('Transmissions retrieved from database and cached', { year, make, model, count: transmissions.length });
    return transmissions;
  } catch (error) {
    logger.error('Service error: getTransmissions', { error, year, make, model });
    throw error;
  }
}

Why This is New:

  • Repository added getTransmissions() method (real data, not hardcoded)
  • Service needs to expose this with caching

Step 7: Update Cache Service

File: backend/src/features/platform/domain/platform-cache.service.ts

The cache service needs updates to handle string-based cache keys instead of ID-based keys.

Cache Key Changes

Current cache keys:

`years`
`makes:${year}`
`models:${year}:${makeId}`
`trims:${year}:${modelId}`
`engines:${year}:${modelId}:${trimId}`

New cache keys:

`years`
`makes:${year}`
`models:${year}:${make}`
`trims:${year}:${make}:${model}`
`engines:${year}:${make}:${model}:${trim}`
`transmissions:${year}:${make}:${model}`  // NEW

Update Cache Method Signatures

Find and update these methods:

getMakes / setMakes

// Signature unchanged (still takes year: number)
async getMakes(year: number): Promise<string[] | null>
async setMakes(year: number, makes: string[]): Promise<void>

getModels / setModels

// OLD
async getModels(year: number, makeId: number): Promise<ModelItem[] | null>
async setModels(year: number, makeId: number, models: ModelItem[]): Promise<void>

// NEW
async getModels(year: number, make: string): Promise<string[] | null>
async setModels(year: number, make: string, models: string[]): Promise<void>

// Implementation change:
const key = `models:${year}:${make}`;  // was makeId

getTrims / setTrims

// OLD
async getTrims(year: number, modelId: number): Promise<TrimItem[] | null>
async setTrims(year: number, modelId: number, trims: TrimItem[]): Promise<void>

// NEW
async getTrims(year: number, make: string, model: string): Promise<string[] | null>
async setTrims(year: number, make: string, model: string, trims: string[]): Promise<void>

// Implementation change:
const key = `trims:${year}:${make}:${model}`;  // was modelId only

getEngines / setEngines

// OLD
async getEngines(year: number, modelId: number, trimId: number): Promise<EngineItem[] | null>
async setEngines(year: number, modelId: number, trimId: number, engines: EngineItem[]): Promise<void>

// NEW
async getEngines(year: number, make: string, model: string, trim: string): Promise<string[] | null>
async setEngines(year: number, make: string, model: string, trim: string, engines: string[]): Promise<void>

// Implementation change:
const key = `engines:${year}:${make}:${model}:${trim}`;  // was modelId:trimId

getTransmissions / setTransmissions (NEW)

async getTransmissions(year: number, make: string, model: string): Promise<string[] | null> {
  const key = `transmissions:${year}:${make}:${model}`;
  const cached = await this.redisClient.get(key);

  if (cached) {
    return JSON.parse(cached);
  }

  return null;
}

async setTransmissions(year: number, make: string, model: string, transmissions: string[]): Promise<void> {
  const key = `transmissions:${year}:${make}:${model}`;
  await this.redisClient.setex(key, this.TTL, JSON.stringify(transmissions));
}

Step 8: Update Documentation Comments

Update the file header:

Old (lines 1-4):

/**
 * @ai-summary Vehicle data service with caching
 * @ai-context Business logic for hierarchical vehicle data queries
 */

New:

/**
 * @ai-summary Vehicle data service with caching for dropdown queries
 * @ai-context String-based cascade queries with Redis caching
 * @ai-migration Updated to use string parameters (not IDs)
 */

Complete Updated Service Structure

After all changes:

/**
 * @ai-summary Vehicle data service with caching for dropdown queries
 * @ai-context String-based cascade queries with Redis caching
 * @ai-migration Updated to use string parameters (not IDs)
 */
import { Pool } from 'pg';
import { VehicleDataRepository } from '../data/vehicle-data.repository';
import { PlatformCacheService } from './platform-cache.service';
import { VINDecodeResult } from '../models/responses';
import { logger } from '../../../core/logging/logger';

export class VehicleDataService {
  private repository: VehicleDataRepository;
  private cache: PlatformCacheService;

  constructor(cache: PlatformCacheService, repository?: VehicleDataRepository) {
    this.cache = cache;
    this.repository = repository || new VehicleDataRepository();
  }

  async getYears(pool: Pool): Promise<number[]> { ... }  // Unchanged

  async getMakes(pool: Pool, year: number): Promise<string[]> { ... }

  async getModels(pool: Pool, year: number, make: string): Promise<string[]> { ... }

  async getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]> { ... }

  async getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]> { ... }

  async getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]> { ... }  // NEW
}

Testing & Verification

Unit Tests

Update existing tests:

File: backend/src/features/platform/tests/unit/vehicle-data.service.test.ts

import { VehicleDataService } from '../../domain/vehicle-data.service';
import { VehicleDataRepository } from '../../data/vehicle-data.repository';
import { PlatformCacheService } from '../../domain/platform-cache.service';

describe('VehicleDataService', () => {
  let service: VehicleDataService;
  let mockRepository: jest.Mocked<VehicleDataRepository>;
  let mockCache: jest.Mocked<PlatformCacheService>;
  let mockPool: any;

  beforeEach(() => {
    mockRepository = {
      getMakes: jest.fn(),
      getModels: jest.fn(),
      getTrims: jest.fn(),
      getEngines: jest.fn(),
      getTransmissions: jest.fn(),
    } as any;

    mockCache = {
      getMakes: jest.fn(),
      setMakes: jest.fn(),
      getModels: jest.fn(),
      setModels: jest.fn(),
      // ... etc
    } as any;

    service = new VehicleDataService(mockCache, mockRepository);
    mockPool = {} as any;
  });

  describe('getMakes', () => {
    it('should return string array from cache', async () => {
      mockCache.getMakes.mockResolvedValue(['Ford', 'Honda']);

      const result = await service.getMakes(mockPool, 2024);

      expect(result).toEqual(['Ford', 'Honda']);
      expect(mockRepository.getMakes).not.toHaveBeenCalled();
    });

    it('should fetch from repository on cache miss', async () => {
      mockCache.getMakes.mockResolvedValue(null);
      mockRepository.getMakes.mockResolvedValue(['Ford', 'Honda']);

      const result = await service.getMakes(mockPool, 2024);

      expect(result).toEqual(['Ford', 'Honda']);
      expect(mockRepository.getMakes).toHaveBeenCalledWith(mockPool, 2024);
      expect(mockCache.setMakes).toHaveBeenCalledWith(2024, ['Ford', 'Honda']);
    });
  });

  describe('getModels', () => {
    it('should accept make string parameter', async () => {
      mockCache.getModels.mockResolvedValue(null);
      mockRepository.getModels.mockResolvedValue(['F-150', 'Mustang']);

      const result = await service.getModels(mockPool, 2024, 'Ford');

      expect(result).toEqual(['F-150', 'Mustang']);
      expect(mockRepository.getModels).toHaveBeenCalledWith(mockPool, 2024, 'Ford');
    });
  });

  describe('getTransmissions', () => {
    it('should return transmission data', async () => {
      mockCache.getTransmissions.mockResolvedValue(null);
      mockRepository.getTransmissions.mockResolvedValue(['10-Speed Automatic']);

      const result = await service.getTransmissions(mockPool, 2024, 'Ford', 'F-150');

      expect(result).toEqual(['10-Speed Automatic']);
    });
  });
});

Run tests:

cd backend
npm test -- vehicle-data.service.test.ts

Integration Test

Test the full flow with real cache:

# Start backend container
make start

# Access backend container
docker exec -it mvp-backend node

# In Node REPL:
const { Pool } = require('pg');
const { VehicleDataService } = require('./src/features/platform/domain/vehicle-data.service');
const { VehicleDataRepository } = require('./src/features/platform/data/vehicle-data.repository');
const { PlatformCacheService } = require('./src/features/platform/domain/platform-cache.service');
const Redis = require('ioredis');

const pool = new Pool({
  host: 'postgres',
  database: 'motovaultpro',
  user: 'postgres',
  password: process.env.POSTGRES_PASSWORD
});

const redis = new Redis({ host: 'redis' });
const cache = new PlatformCacheService(redis);
const repo = new VehicleDataRepository();
const service = new VehicleDataService(cache, repo);

// Test getMakes
await service.getMakes(pool, 2024);
// First call: fetches from DB, caches result
// Second call: returns from cache
await service.getMakes(pool, 2024);

// Test getModels with string parameter
await service.getModels(pool, 2024, 'Ford');

// Test getTrims
await service.getTrims(pool, 2024, 'Ford', 'F-150');

// Test getEngines
await service.getEngines(pool, 2024, 'Ford', 'F-150', 'XLT');

// Test getTransmissions (NEW)
await service.getTransmissions(pool, 2024, 'Ford', 'F-150');

Completion Checklist

Before signaling completion:

  • All method signatures updated (string parameters, not IDs)
  • Return types changed to string[] (removed object types)
  • getMakes() updated
  • getModels() updated with make parameter
  • getTrims() updated with make and model parameters
  • getEngines() updated with make, model, trim parameters
  • getTransmissions() method added (new)
  • Cache service signatures updated
  • Cache keys use strings (not IDs)
  • Unit tests pass
  • Integration tests verify caching works
  • TypeScript compiles with no errors
  • File documentation updated

Common Issues

Issue: TypeScript error "Type 'string[]' is not assignable to type 'MakeItem[]'"

Cause: Cache service still using old types

Solution:

  • Update cache service method signatures (Step 7)
  • Ensure cache methods return string[] | null not MakeItem[] | null

Issue: Cache keys collision

Cause: String-based keys might collide with old ID-based keys if Redis not cleared

Solution:

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

# Or clear specific pattern
docker exec mvp-redis redis-cli KEYS "makes:*" | xargs docker exec mvp-redis redis-cli DEL

Issue: Service tests fail with parameter mismatch

Cause: Tests still passing old ID parameters

Solution:

  • Update test calls to use strings: service.getModels(pool, 2024, 'Ford') not service.getModels(pool, 2024, 1)

Handoff to Agent 4

Once complete, provide this information:

Updated Service Contract

Methods:

getYears(pool: Pool): Promise<number[]>
getMakes(pool: Pool, year: number): Promise<string[]>
getModels(pool: Pool, year: number, make: string): Promise<string[]>
getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]>
getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]>
getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]>

Key Changes for Agent 4:

  • All dropdown methods return string[]
  • Parameters use strings (make, model, trim) not IDs
  • getTransmissions() is a new method
  • Caching layer handles string-based keys

Verification Command

# Agent 4 can verify service is ready:
cd backend && npm run build
# Should compile with no errors in vehicle-data.service.ts

Completion Message Template

Agent 3 (Platform Service): COMPLETE

Files Modified:
- backend/src/features/platform/domain/vehicle-data.service.ts
- backend/src/features/platform/domain/platform-cache.service.ts

Changes Made:
- Updated all method signatures to accept strings (not IDs)
- Changed return types to string[] (removed object types)
- Updated cache keys to use string-based parameters
- Added getTransmissions() method with caching
- All methods still use Redis caching (TTL unchanged)

Verification:
✓ TypeScript compiles successfully
✓ Unit tests pass
✓ Integration tests confirm caching works
✓ Cache keys use string parameters

Agent 4 (Vehicles API) can now update controllers to use new service signatures.

Breaking Changes for Agent 4:
- Service methods accept strings: make, model, trim (not makeId, modelId, trimId)
- Service methods return string[] (not {id, name}[])
- New method: getTransmissions()

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