/** * @ai-summary Business logic for vehicles feature * @ai-context Handles VIN decoding, caching, and business rules */ import { VehiclesRepository } from '../data/vehicles.repository'; import { getVINDecodeService, getPool } from '../../platform'; 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'; import { normalizeMakeName, normalizeModelName } from './name-normalizer'; export class VehiclesService { private readonly cachePrefix = 'vehicles'; private readonly listCacheTTL = 300; // 5 minutes constructor(private repository: VehiclesRepository) { // VIN decode service is now provided by platform feature } async createVehicle(data: CreateVehicleRequest, userId: string): Promise { logger.info('Creating vehicle', { userId, vin: data.vin, licensePlate: (data as any).licensePlate }); let make: string | undefined; let model: string | undefined; let year: number | undefined; if (data.vin) { // Validate VIN if provided if (!isValidVIN(data.vin)) { throw new Error('Invalid VIN format'); } // Duplicate check only when VIN is present const existing = await this.repository.findByUserAndVIN(userId, data.vin); if (existing) { throw new Error('Vehicle with this VIN already exists'); } // Attempt VIN decode to enrich fields using platform service const vinDecodeService = getVINDecodeService(); const pool = getPool(); const vinDecodeResult = await vinDecodeService.decodeVIN(pool, data.vin); if (vinDecodeResult.success && vinDecodeResult.result) { make = normalizeMakeName(vinDecodeResult.result.make); model = normalizeModelName(vinDecodeResult.result.model); year = vinDecodeResult.result.year ?? undefined; // VIN caching is now handled by platform feature } } // Create vehicle (VIN optional). Client-sent make/model/year override decode if provided. const inputMake = (data as any).make ?? make; const inputModel = (data as any).model ?? model; const vehicle = await this.repository.create({ ...data, userId, make: normalizeMakeName(inputMake), model: normalizeModelName(inputModel), year: (data as any).year ?? year, }); // 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: Vehicle) => 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'); } // Normalize any provided name fields const normalized: UpdateVehicleRequest = { ...data } as any; if (data.make !== undefined) { (normalized as any).make = normalizeMakeName(data.make); } if (data.model !== undefined) { (normalized as any).model = normalizeModelName(data.model); } // Update vehicle const updated = await this.repository.update(id, normalized); 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(_year: number): Promise<{ id: number; name: string }[]> { // TODO: Implement using platform VehicleDataService // For now, return empty array to allow migration to complete logger.warn('Dropdown makes not yet implemented via platform feature'); return []; } async getDropdownModels(_year: number, _makeId: number): Promise<{ id: number; name: string }[]> { // TODO: Implement using platform VehicleDataService logger.warn('Dropdown models not yet implemented via platform feature'); return []; } async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> { // TODO: Implement using platform VehicleDataService logger.warn('Dropdown transmissions not yet implemented via platform feature'); return []; } async getDropdownEngines(_year: number, _makeId: number, _modelId: number, _trimId: number): Promise<{ name: string }[]> { // TODO: Implement using platform VehicleDataService logger.warn('Dropdown engines not yet implemented via platform feature'); return []; } async getDropdownTrims(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> { // TODO: Implement using platform VehicleDataService logger.warn('Dropdown trims not yet implemented via platform feature'); return []; } async getDropdownYears(): Promise { try { logger.info('Getting dropdown years'); // Fallback: generate recent years const currentYear = new Date().getFullYear(); const years: number[] = []; for (let y = currentYear + 1; y >= 1980; y--) years.push(y); return years; } catch (error) { logger.error('Failed to get dropdown years', { error }); const currentYear = new Date().getFullYear(); const years: number[] = []; for (let y = currentYear + 1; y >= 1980; y--) years.push(y); return years; } } async decodeVIN(vin: string): Promise<{ vin: string; success: boolean; year?: number; make?: string; model?: string; trimLevel?: string; engine?: string; transmission?: string; confidence?: number; error?: string; }> { try { logger.info('Decoding VIN', { vin }); // Use platform feature's VIN decode service const vinDecodeService = getVINDecodeService(); const pool = getPool(); const result = await vinDecodeService.decodeVIN(pool, vin); if (result.success && result.result) { return { vin, success: true, year: result.result.year ?? undefined, make: result.result.make ?? undefined, model: result.result.model ?? undefined, trimLevel: result.result.trim_name ?? undefined, engine: result.result.engine_description ?? undefined, confidence: 85 // High confidence since we have good data }; } else { return { vin, success: false, error: result.error || 'Unable to decode VIN' }; } } catch (error) { logger.error('Failed to decode VIN', { vin, error }); return { vin, success: false, error: 'VIN decode service unavailable' }; } } 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(), }; } }