296 lines
11 KiB
TypeScript
296 lines
11 KiB
TypeScript
/**
|
|
* @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'
|
|
});
|
|
}
|
|
}
|
|
}
|