/** * @ai-summary Business logic for vehicles feature * @ai-context Handles VIN decoding, caching, and business rules */ import { VehiclesRepository } from '../data/vehicles.repository'; 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'; import { getVehicleDataService, getPool } from '../../platform'; 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 }); 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'); } } // Create vehicle with user-provided data const vehicle = await this.repository.create({ ...data, userId, make: data.make ? normalizeMakeName(data.make) : undefined, model: data.model ? normalizeModelName(data.model) : undefined, }); // 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 { const vehicleDataService = getVehicleDataService(); const pool = getPool(); logger.info('Fetching dropdown makes via platform module', { year }); return vehicleDataService.getMakes(pool, year); } async getDropdownModels(year: number, make: string): Promise { const vehicleDataService = getVehicleDataService(); const pool = getPool(); logger.info('Fetching dropdown models via platform module', { year, make }); return vehicleDataService.getModels(pool, year, make); } async getDropdownTransmissions(year: number, make: string, model: string, trim: string): Promise { const vehicleDataService = getVehicleDataService(); const pool = getPool(); logger.info('Fetching dropdown transmissions via platform module', { year, make, model, trim }); return vehicleDataService.getTransmissionsForTrim(pool, year, make, model, trim); } async getDropdownEngines(year: number, make: string, model: string, trim: string): Promise { 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); } async getDropdownTrims(year: number, make: string, model: string): Promise { const vehicleDataService = getVehicleDataService(); const pool = getPool(); logger.info('Fetching dropdown trims via platform module', { year, make, model }); return vehicleDataService.getTrims(pool, year, make, model); } async getDropdownOptions( year: number, make: string, model: string, trim: string, engine?: string, transmission?: string ): Promise<{ engines: string[]; transmissions: string[] }> { const vehicleDataService = getVehicleDataService(); const pool = getPool(); logger.info('Fetching dropdown options via platform module', { year, make, model, trim, engine, transmission }); return vehicleDataService.getOptions(pool, year, make, model, trim, engine, transmission); } async getDropdownYears(): Promise { const vehicleDataService = getVehicleDataService(); const pool = getPool(); logger.info('Fetching dropdown years via platform module'); return vehicleDataService.getYears(pool); } 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(), }; } }