/** * @ai-summary Fastify route handlers for vehicles API * @ai-context HTTP request/response handling with Fastify reply methods */ import { FastifyRequest, FastifyReply } from 'fastify'; import { VehiclesService } from '../domain/vehicles.service'; import { VehiclesRepository } from '../data/vehicles.repository'; import { pool } from '../../../core/config/database'; import { logger } from '../../../core/logging/logger'; import { CreateVehicleBody, UpdateVehicleBody, VehicleParams } from '../domain/vehicles.types'; export class VehiclesController { private vehiclesService: VehiclesService; constructor() { const repository = new VehiclesRepository(pool); this.vehiclesService = new VehiclesService(repository); } async getUserVehicles(request: FastifyRequest, reply: FastifyReply) { try { const userId = (request as any).user.sub; const vehicles = await this.vehiclesService.getUserVehicles(userId); return reply.code(200).send(vehicles); } catch (error) { logger.error('Error getting user vehicles', { error, userId: (request as any).user?.sub }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to get vehicles' }); } } async createVehicle(request: FastifyRequest<{ Body: CreateVehicleBody }>, reply: FastifyReply) { try { // Require either a valid 17-char VIN or a non-empty license plate const vin = request.body?.vin?.trim(); const plate = request.body?.licensePlate?.trim(); const hasValidVin = !!vin && vin.length === 17; const hasPlate = !!plate && plate.length > 0; if (!hasValidVin && !hasPlate) { return reply.code(400).send({ error: 'Bad Request', message: 'Either a valid 17-character VIN or a license plate is required' }); } const userId = (request as any).user.sub; const vehicle = await this.vehiclesService.createVehicle(request.body, userId); return reply.code(201).send(vehicle); } catch (error: any) { logger.error('Error creating vehicle', { error, userId: (request as any).user?.sub }); if (error.message === 'Invalid VIN format') { return reply.code(400).send({ error: 'Bad Request', message: error.message }); } if (error.message === 'Vehicle with this VIN already exists') { return reply.code(400).send({ error: 'Bad Request', message: error.message }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to create vehicle' }); } } async getVehicle(request: FastifyRequest<{ Params: VehicleParams }>, reply: FastifyReply) { try { const userId = (request as any).user.sub; const { id } = request.params; const vehicle = await this.vehiclesService.getVehicle(id, userId); return reply.code(200).send(vehicle); } catch (error: any) { logger.error('Error getting vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub }); if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') { return reply.code(404).send({ error: 'Not Found', message: 'Vehicle not found' }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to get vehicle' }); } } async updateVehicle(request: FastifyRequest<{ Params: VehicleParams; Body: UpdateVehicleBody }>, reply: FastifyReply) { try { const userId = (request as any).user.sub; const { id } = request.params; const vehicle = await this.vehiclesService.updateVehicle(id, request.body, userId); return reply.code(200).send(vehicle); } catch (error: any) { logger.error('Error updating vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub }); if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') { return reply.code(404).send({ error: 'Not Found', message: 'Vehicle not found' }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to update vehicle' }); } } async deleteVehicle(request: FastifyRequest<{ Params: VehicleParams }>, reply: FastifyReply) { try { const userId = (request as any).user.sub; const { id } = request.params; await this.vehiclesService.deleteVehicle(id, userId); return reply.code(204).send(); } catch (error: any) { logger.error('Error deleting vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub }); if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') { return reply.code(404).send({ error: 'Not Found', message: 'Vehicle not found' }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to delete vehicle' }); } } 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' }); } } 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' }); } } 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' }); } } 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' }); } } 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' }); } } async getDropdownYears(_request: FastifyRequest, reply: FastifyReply) { try { // Use platform client through VehiclesService's integration const years = await this.vehiclesService.getDropdownYears(); return reply.code(200).send(years); } catch (error) { logger.error('Error getting dropdown years', { error }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to get years' }); } } async decodeVIN(request: FastifyRequest<{ Body: { vin: string } }>, reply: FastifyReply) { try { const { vin } = request.body; if (!vin || vin.length !== 17) { return reply.code(400).send({ vin: vin || '', success: false, error: 'VIN must be exactly 17 characters' }); } const result = await this.vehiclesService.decodeVIN(vin); return reply.code(200).send(result); } catch (error: any) { logger.error('Error decoding VIN', { error, vin: request.body?.vin }); return reply.code(500).send({ vin: request.body?.vin || '', success: false, error: 'VIN decode failed' }); } } }