/** * @ai-summary Business logic for vehicles feature * @ai-context Handles VIN decoding, caching, and business rules */ import { VehiclesRepository } from '../data/vehicles.repository'; import { vpicClient } from '../external/vpic/vpic.client'; import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, VehicleResponse } from './vehicles.types'; import { logger } from '../../../core/logging/logger'; import { cacheService } from '../../../core/config/redis'; import { isValidVIN } from '../../../shared-minimal/utils/validators'; export class VehiclesService { private readonly cachePrefix = 'vehicles'; private readonly listCacheTTL = 300; // 5 minutes constructor(private repository: VehiclesRepository) {} async createVehicle(data: CreateVehicleRequest, userId: string): Promise { logger.info('Creating vehicle', { userId, vin: data.vin }); // Validate VIN if (!isValidVIN(data.vin)) { throw new Error('Invalid VIN format'); } // Check for duplicate const existing = await this.repository.findByUserAndVIN(userId, data.vin); if (existing) { throw new Error('Vehicle with this VIN already exists'); } // Decode VIN const vinData = await vpicClient.decodeVIN(data.vin); // Create vehicle with decoded data const vehicle = await this.repository.create({ ...data, userId, make: vinData?.make, model: vinData?.model, year: vinData?.year, }); // Cache VIN decode result if (vinData) { await this.repository.cacheVINDecode(data.vin, vinData); } // Invalidate user's vehicle list cache await this.invalidateUserCache(userId); return this.toResponse(vehicle); } async getUserVehicles(userId: string): Promise { const cacheKey = `${this.cachePrefix}:user:${userId}`; // Check cache const cached = await cacheService.get(cacheKey); if (cached) { logger.debug('Vehicle list cache hit', { userId }); return cached; } // Get from database const vehicles = await this.repository.findByUserId(userId); const response = vehicles.map(v => this.toResponse(v)); // Cache result await cacheService.set(cacheKey, response, this.listCacheTTL); return response; } async getVehicle(id: string, userId: string): Promise { const vehicle = await this.repository.findById(id); if (!vehicle) { throw new Error('Vehicle not found'); } if (vehicle.userId !== userId) { throw new Error('Unauthorized'); } return this.toResponse(vehicle); } async updateVehicle( id: string, data: UpdateVehicleRequest, userId: string ): Promise { // Verify ownership const existing = await this.repository.findById(id); if (!existing) { throw new Error('Vehicle not found'); } if (existing.userId !== userId) { throw new Error('Unauthorized'); } // Update vehicle const updated = await this.repository.update(id, data); if (!updated) { throw new Error('Update failed'); } // Invalidate cache await this.invalidateUserCache(userId); return this.toResponse(updated); } async deleteVehicle(id: string, userId: string): Promise { // Verify ownership const existing = await this.repository.findById(id); if (!existing) { throw new Error('Vehicle not found'); } if (existing.userId !== userId) { throw new Error('Unauthorized'); } // Soft delete await this.repository.softDelete(id); // Invalidate cache await this.invalidateUserCache(userId); } private async invalidateUserCache(userId: string): Promise { const cacheKey = `${this.cachePrefix}:user:${userId}`; await cacheService.del(cacheKey); } async getDropdownMakes(): Promise<{ id: number; name: string }[]> { try { logger.info('Getting dropdown makes'); const makes = await vpicClient.getAllMakes(); return makes.map(make => ({ id: make.Make_ID, name: make.Make_Name })); } catch (error) { logger.error('Failed to get dropdown makes', { error }); throw new Error('Failed to load makes'); } } async getDropdownModels(make: string): Promise<{ id: number; name: string }[]> { try { logger.info('Getting dropdown models', { make }); const models = await vpicClient.getModelsForMake(make); return models.map(model => ({ id: model.Model_ID, name: model.Model_Name })); } catch (error) { logger.error('Failed to get dropdown models', { make, error }); throw new Error('Failed to load models'); } } async getDropdownTransmissions(): Promise<{ id: number; name: string }[]> { try { logger.info('Getting dropdown transmissions'); const transmissions = await vpicClient.getTransmissionTypes(); return transmissions.map(transmission => ({ id: transmission.Id, name: transmission.Name })); } catch (error) { logger.error('Failed to get dropdown transmissions', { error }); throw new Error('Failed to load transmissions'); } } async getDropdownEngines(): Promise<{ id: number; name: string }[]> { try { logger.info('Getting dropdown engines'); const engines = await vpicClient.getEngineConfigurations(); return engines.map(engine => ({ id: engine.Id, name: engine.Name })); } catch (error) { logger.error('Failed to get dropdown engines', { error }); throw new Error('Failed to load engines'); } } async getDropdownTrims(): Promise<{ id: number; name: string }[]> { try { logger.info('Getting dropdown trims'); const trims = await vpicClient.getTrimLevels(); return trims.map(trim => ({ id: trim.Id, name: trim.Name })); } catch (error) { logger.error('Failed to get dropdown trims', { error }); throw new Error('Failed to load trims'); } } private toResponse(vehicle: Vehicle): VehicleResponse { return { id: vehicle.id, userId: vehicle.userId, vin: vehicle.vin, make: vehicle.make, model: vehicle.model, year: vehicle.year, engine: vehicle.engine, transmission: vehicle.transmission, trimLevel: vehicle.trimLevel, driveType: vehicle.driveType, fuelType: vehicle.fuelType, nickname: vehicle.nickname, color: vehicle.color, licensePlate: vehicle.licensePlate, odometerReading: vehicle.odometerReading, isActive: vehicle.isActive, createdAt: vehicle.createdAt.toISOString(), updatedAt: vehicle.updatedAt.toISOString(), }; } }