917 lines
28 KiB
TypeScript
917 lines
28 KiB
TypeScript
/**
|
|
* @ai-summary Catalog API controller for platform vehicle data management
|
|
* @ai-context Handles HTTP requests for CRUD operations on makes, models, years, trims, engines
|
|
*/
|
|
|
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
import { VehicleCatalogService } from '../domain/vehicle-catalog.service';
|
|
import { CatalogImportService } from '../domain/catalog-import.service';
|
|
import { logger } from '../../../core/logging/logger';
|
|
|
|
export class CatalogController {
|
|
private importService: CatalogImportService | null = null;
|
|
|
|
constructor(private catalogService: VehicleCatalogService) {}
|
|
|
|
setImportService(importService: CatalogImportService): void {
|
|
this.importService = importService;
|
|
}
|
|
|
|
// MAKES ENDPOINTS
|
|
|
|
async getMakes(_request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
|
try {
|
|
const makes = await this.catalogService.getAllMakes();
|
|
reply.code(200).send({ makes });
|
|
} catch (error) {
|
|
logger.error('Error getting makes', { error });
|
|
reply.code(500).send({ error: 'Failed to retrieve makes' });
|
|
}
|
|
}
|
|
|
|
async createMake(
|
|
request: FastifyRequest<{ Body: { name: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const { name } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
reply.code(400).send({ error: 'Make name is required' });
|
|
return;
|
|
}
|
|
|
|
const make = await this.catalogService.createMake(name.trim(), actorId);
|
|
reply.code(201).send(make);
|
|
} catch (error) {
|
|
logger.error('Error creating make', { error });
|
|
reply.code(500).send({ error: 'Failed to create make' });
|
|
}
|
|
}
|
|
|
|
async updateMake(
|
|
request: FastifyRequest<{ Params: { makeId: string }; Body: { name: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const makeId = parseInt(request.params.makeId);
|
|
const { name } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(makeId)) {
|
|
reply.code(400).send({ error: 'Invalid make ID' });
|
|
return;
|
|
}
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
reply.code(400).send({ error: 'Make name is required' });
|
|
return;
|
|
}
|
|
|
|
const make = await this.catalogService.updateMake(makeId, name.trim(), actorId);
|
|
reply.code(200).send(make);
|
|
} catch (error: any) {
|
|
logger.error('Error updating make', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to update make' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteMake(
|
|
request: FastifyRequest<{ Params: { makeId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const makeId = parseInt(request.params.makeId);
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(makeId)) {
|
|
reply.code(400).send({ error: 'Invalid make ID' });
|
|
return;
|
|
}
|
|
|
|
await this.catalogService.deleteMake(makeId, actorId);
|
|
reply.code(204).send();
|
|
} catch (error: any) {
|
|
logger.error('Error deleting make', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else if (error.message?.includes('existing models')) {
|
|
reply.code(409).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to delete make' });
|
|
}
|
|
}
|
|
}
|
|
|
|
// MODELS ENDPOINTS
|
|
|
|
async getModels(
|
|
request: FastifyRequest<{ Params: { makeId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const makeId = parseInt(request.params.makeId);
|
|
|
|
if (isNaN(makeId)) {
|
|
reply.code(400).send({ error: 'Invalid make ID' });
|
|
return;
|
|
}
|
|
|
|
const models = await this.catalogService.getModelsByMake(makeId);
|
|
reply.code(200).send({ models });
|
|
} catch (error) {
|
|
logger.error('Error getting models', { error });
|
|
reply.code(500).send({ error: 'Failed to retrieve models' });
|
|
}
|
|
}
|
|
|
|
async createModel(
|
|
request: FastifyRequest<{ Body: { makeId: number; name: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const { makeId, name } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
const parsedMakeId = Number(makeId);
|
|
|
|
if (!Number.isFinite(parsedMakeId) || parsedMakeId <= 0) {
|
|
reply.code(400).send({ error: 'Valid make ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
reply.code(400).send({ error: 'Make ID and model name are required' });
|
|
return;
|
|
}
|
|
|
|
const model = await this.catalogService.createModel(parsedMakeId, name.trim(), actorId);
|
|
reply.code(201).send(model);
|
|
} catch (error: any) {
|
|
logger.error('Error creating model', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to create model' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async updateModel(
|
|
request: FastifyRequest<{ Params: { modelId: string }; Body: { makeId: number; name: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const modelId = parseInt(request.params.modelId);
|
|
const { makeId, name } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
const parsedMakeId = Number(makeId);
|
|
|
|
if (isNaN(modelId)) {
|
|
reply.code(400).send({ error: 'Invalid model ID' });
|
|
return;
|
|
}
|
|
|
|
if (!Number.isFinite(parsedMakeId) || parsedMakeId <= 0) {
|
|
reply.code(400).send({ error: 'Valid make ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
reply.code(400).send({ error: 'Make ID and model name are required' });
|
|
return;
|
|
}
|
|
|
|
const model = await this.catalogService.updateModel(modelId, parsedMakeId, name.trim(), actorId);
|
|
reply.code(200).send(model);
|
|
} catch (error: any) {
|
|
logger.error('Error updating model', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to update model' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteModel(
|
|
request: FastifyRequest<{ Params: { modelId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const modelId = parseInt(request.params.modelId);
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(modelId)) {
|
|
reply.code(400).send({ error: 'Invalid model ID' });
|
|
return;
|
|
}
|
|
|
|
await this.catalogService.deleteModel(modelId, actorId);
|
|
reply.code(204).send();
|
|
} catch (error: any) {
|
|
logger.error('Error deleting model', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else if (error.message?.includes('existing years')) {
|
|
reply.code(409).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to delete model' });
|
|
}
|
|
}
|
|
}
|
|
|
|
// YEARS ENDPOINTS
|
|
|
|
async getYears(
|
|
request: FastifyRequest<{ Params: { modelId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const modelId = parseInt(request.params.modelId);
|
|
|
|
if (isNaN(modelId)) {
|
|
reply.code(400).send({ error: 'Invalid model ID' });
|
|
return;
|
|
}
|
|
|
|
const years = await this.catalogService.getYearsByModel(modelId);
|
|
reply.code(200).send({ years });
|
|
} catch (error) {
|
|
logger.error('Error getting years', { error });
|
|
reply.code(500).send({ error: 'Failed to retrieve years' });
|
|
}
|
|
}
|
|
|
|
async createYear(
|
|
request: FastifyRequest<{ Body: { modelId: number; year: number } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const { modelId, year } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
const parsedModelId = Number(modelId);
|
|
const parsedYear = Number(year);
|
|
|
|
if (!Number.isFinite(parsedModelId) || parsedModelId <= 0) {
|
|
reply.code(400).send({ error: 'Valid model ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!Number.isInteger(parsedYear) || parsedYear < 1900 || parsedYear > 2100) {
|
|
reply.code(400).send({ error: 'Valid model ID and year (1900-2100) are required' });
|
|
return;
|
|
}
|
|
|
|
const yearData = await this.catalogService.createYear(parsedModelId, parsedYear, actorId);
|
|
reply.code(201).send(yearData);
|
|
} catch (error: any) {
|
|
logger.error('Error creating year', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to create year' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async updateYear(
|
|
request: FastifyRequest<{ Params: { yearId: string }; Body: { modelId: number; year: number } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const yearId = parseInt(request.params.yearId);
|
|
const { modelId, year } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
const parsedModelId = Number(modelId);
|
|
const parsedYear = Number(year);
|
|
|
|
if (isNaN(yearId)) {
|
|
reply.code(400).send({ error: 'Invalid year ID' });
|
|
return;
|
|
}
|
|
|
|
if (!Number.isFinite(parsedModelId) || parsedModelId <= 0) {
|
|
reply.code(400).send({ error: 'Valid model ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!Number.isInteger(parsedYear) || parsedYear < 1900 || parsedYear > 2100) {
|
|
reply.code(400).send({ error: 'Valid model ID and year (1900-2100) are required' });
|
|
return;
|
|
}
|
|
|
|
const yearData = await this.catalogService.updateYear(yearId, parsedModelId, parsedYear, actorId);
|
|
reply.code(200).send(yearData);
|
|
} catch (error: any) {
|
|
logger.error('Error updating year', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to update year' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteYear(
|
|
request: FastifyRequest<{ Params: { yearId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const yearId = parseInt(request.params.yearId);
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(yearId)) {
|
|
reply.code(400).send({ error: 'Invalid year ID' });
|
|
return;
|
|
}
|
|
|
|
await this.catalogService.deleteYear(yearId, actorId);
|
|
reply.code(204).send();
|
|
} catch (error: any) {
|
|
logger.error('Error deleting year', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else if (error.message?.includes('existing trims')) {
|
|
reply.code(409).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to delete year' });
|
|
}
|
|
}
|
|
}
|
|
|
|
// TRIMS ENDPOINTS
|
|
|
|
async getTrims(
|
|
request: FastifyRequest<{ Params: { yearId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const yearId = parseInt(request.params.yearId);
|
|
|
|
if (isNaN(yearId)) {
|
|
reply.code(400).send({ error: 'Invalid year ID' });
|
|
return;
|
|
}
|
|
|
|
const trims = await this.catalogService.getTrimsByYear(yearId);
|
|
reply.code(200).send({ trims });
|
|
} catch (error) {
|
|
logger.error('Error getting trims', { error });
|
|
reply.code(500).send({ error: 'Failed to retrieve trims' });
|
|
}
|
|
}
|
|
|
|
async createTrim(
|
|
request: FastifyRequest<{ Body: { yearId: number; name: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const { yearId, name } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
const parsedYearId = Number(yearId);
|
|
|
|
if (!Number.isFinite(parsedYearId) || parsedYearId <= 0) {
|
|
reply.code(400).send({ error: 'Valid year ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
reply.code(400).send({ error: 'Year ID and trim name are required' });
|
|
return;
|
|
}
|
|
|
|
const trim = await this.catalogService.createTrim(parsedYearId, name.trim(), actorId);
|
|
reply.code(201).send(trim);
|
|
} catch (error: any) {
|
|
logger.error('Error creating trim', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to create trim' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async updateTrim(
|
|
request: FastifyRequest<{ Params: { trimId: string }; Body: { yearId: number; name: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const trimId = parseInt(request.params.trimId);
|
|
const { yearId, name } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
const parsedYearId = Number(yearId);
|
|
|
|
if (isNaN(trimId)) {
|
|
reply.code(400).send({ error: 'Invalid trim ID' });
|
|
return;
|
|
}
|
|
|
|
if (!Number.isFinite(parsedYearId) || parsedYearId <= 0) {
|
|
reply.code(400).send({ error: 'Valid year ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
reply.code(400).send({ error: 'Year ID and trim name are required' });
|
|
return;
|
|
}
|
|
|
|
const trim = await this.catalogService.updateTrim(trimId, parsedYearId, name.trim(), actorId);
|
|
reply.code(200).send(trim);
|
|
} catch (error: any) {
|
|
logger.error('Error updating trim', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to update trim' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteTrim(
|
|
request: FastifyRequest<{ Params: { trimId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const trimId = parseInt(request.params.trimId);
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(trimId)) {
|
|
reply.code(400).send({ error: 'Invalid trim ID' });
|
|
return;
|
|
}
|
|
|
|
await this.catalogService.deleteTrim(trimId, actorId);
|
|
reply.code(204).send();
|
|
} catch (error: any) {
|
|
logger.error('Error deleting trim', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else if (error.message?.includes('existing engines')) {
|
|
reply.code(409).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to delete trim' });
|
|
}
|
|
}
|
|
}
|
|
|
|
// ENGINES ENDPOINTS
|
|
|
|
async getEngines(
|
|
request: FastifyRequest<{ Params: { trimId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const trimId = parseInt(request.params.trimId);
|
|
|
|
if (isNaN(trimId)) {
|
|
reply.code(400).send({ error: 'Invalid trim ID' });
|
|
return;
|
|
}
|
|
|
|
const engines = await this.catalogService.getEnginesByTrim(trimId);
|
|
reply.code(200).send({ engines });
|
|
} catch (error) {
|
|
logger.error('Error getting engines', { error });
|
|
reply.code(500).send({ error: 'Failed to retrieve engines' });
|
|
}
|
|
}
|
|
|
|
async createEngine(
|
|
request: FastifyRequest<{ Body: { trimId: number; name: string; description?: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const { trimId, name, description } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
const parsedTrimId = Number(trimId);
|
|
|
|
if (!Number.isFinite(parsedTrimId) || parsedTrimId <= 0) {
|
|
reply.code(400).send({ error: 'Valid trim ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
reply.code(400).send({ error: 'Trim ID and engine name are required' });
|
|
return;
|
|
}
|
|
|
|
const engine = await this.catalogService.createEngine(parsedTrimId, name.trim(), description, actorId);
|
|
reply.code(201).send(engine);
|
|
} catch (error: any) {
|
|
logger.error('Error creating engine', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to create engine' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async updateEngine(
|
|
request: FastifyRequest<{ Params: { engineId: string }; Body: { trimId: number; name: string; description?: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const engineId = parseInt(request.params.engineId);
|
|
const { trimId, name, description } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
const parsedTrimId = Number(trimId);
|
|
|
|
if (isNaN(engineId)) {
|
|
reply.code(400).send({ error: 'Invalid engine ID' });
|
|
return;
|
|
}
|
|
|
|
if (!Number.isFinite(parsedTrimId) || parsedTrimId <= 0) {
|
|
reply.code(400).send({ error: 'Valid trim ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
reply.code(400).send({ error: 'Trim ID and engine name are required' });
|
|
return;
|
|
}
|
|
|
|
const engine = await this.catalogService.updateEngine(
|
|
engineId,
|
|
parsedTrimId,
|
|
name.trim(),
|
|
description,
|
|
actorId
|
|
);
|
|
reply.code(200).send(engine);
|
|
} catch (error: any) {
|
|
logger.error('Error updating engine', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to update engine' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteEngine(
|
|
request: FastifyRequest<{ Params: { engineId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const engineId = parseInt(request.params.engineId);
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(engineId)) {
|
|
reply.code(400).send({ error: 'Invalid engine ID' });
|
|
return;
|
|
}
|
|
|
|
await this.catalogService.deleteEngine(engineId, actorId);
|
|
reply.code(204).send();
|
|
} catch (error: any) {
|
|
logger.error('Error deleting engine', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to delete engine' });
|
|
}
|
|
}
|
|
}
|
|
|
|
// CHANGE LOG ENDPOINT
|
|
|
|
async getChangeLogs(
|
|
request: FastifyRequest<{ Querystring: { limit?: string; offset?: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const limit = parseInt(request.query.limit || '100');
|
|
const offset = parseInt(request.query.offset || '0');
|
|
|
|
const result = await this.catalogService.getChangeLogs(limit, offset);
|
|
reply.code(200).send(result);
|
|
} catch (error) {
|
|
logger.error('Error getting change logs', { error });
|
|
reply.code(500).send({ error: 'Failed to retrieve change logs' });
|
|
}
|
|
}
|
|
|
|
// SEARCH ENDPOINT
|
|
|
|
async searchCatalog(
|
|
request: FastifyRequest<{ Querystring: { q?: string; page?: string; pageSize?: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const query = request.query.q || '';
|
|
const page = parseInt(request.query.page || '1');
|
|
const pageSize = Math.min(parseInt(request.query.pageSize || '50'), 100); // Max 100 per page
|
|
|
|
if (isNaN(page) || page < 1) {
|
|
reply.code(400).send({ error: 'Invalid page number' });
|
|
return;
|
|
}
|
|
|
|
if (isNaN(pageSize) || pageSize < 1) {
|
|
reply.code(400).send({ error: 'Invalid page size' });
|
|
return;
|
|
}
|
|
|
|
const result = await this.catalogService.searchCatalog(query, page, pageSize);
|
|
reply.code(200).send(result);
|
|
} catch (error) {
|
|
logger.error('Error searching catalog', { error });
|
|
reply.code(500).send({ error: 'Failed to search catalog' });
|
|
}
|
|
}
|
|
|
|
// CASCADE DELETE ENDPOINTS
|
|
|
|
async deleteMakeCascade(
|
|
request: FastifyRequest<{ Params: { makeId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const makeId = parseInt(request.params.makeId);
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(makeId)) {
|
|
reply.code(400).send({ error: 'Invalid make ID' });
|
|
return;
|
|
}
|
|
|
|
const result = await this.catalogService.deleteMakeCascade(makeId, actorId);
|
|
reply.code(200).send(result);
|
|
} catch (error: any) {
|
|
logger.error('Error cascade deleting make', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to cascade delete make' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteModelCascade(
|
|
request: FastifyRequest<{ Params: { modelId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const modelId = parseInt(request.params.modelId);
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(modelId)) {
|
|
reply.code(400).send({ error: 'Invalid model ID' });
|
|
return;
|
|
}
|
|
|
|
const result = await this.catalogService.deleteModelCascade(modelId, actorId);
|
|
reply.code(200).send(result);
|
|
} catch (error: any) {
|
|
logger.error('Error cascade deleting model', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to cascade delete model' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteYearCascade(
|
|
request: FastifyRequest<{ Params: { yearId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const yearId = parseInt(request.params.yearId);
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(yearId)) {
|
|
reply.code(400).send({ error: 'Invalid year ID' });
|
|
return;
|
|
}
|
|
|
|
const result = await this.catalogService.deleteYearCascade(yearId, actorId);
|
|
reply.code(200).send(result);
|
|
} catch (error: any) {
|
|
logger.error('Error cascade deleting year', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to cascade delete year' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteTrimCascade(
|
|
request: FastifyRequest<{ Params: { trimId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const trimId = parseInt(request.params.trimId);
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (isNaN(trimId)) {
|
|
reply.code(400).send({ error: 'Invalid trim ID' });
|
|
return;
|
|
}
|
|
|
|
const result = await this.catalogService.deleteTrimCascade(trimId, actorId);
|
|
reply.code(200).send(result);
|
|
} catch (error: any) {
|
|
logger.error('Error cascade deleting trim', { error });
|
|
if (error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: 'Failed to cascade delete trim' });
|
|
}
|
|
}
|
|
}
|
|
|
|
// IMPORT/EXPORT ENDPOINTS
|
|
|
|
async importPreview(
|
|
request: FastifyRequest,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
if (!this.importService) {
|
|
reply.code(500).send({ error: 'Import service not configured' });
|
|
return;
|
|
}
|
|
|
|
const data = await request.file();
|
|
if (!data) {
|
|
reply.code(400).send({ error: 'No file uploaded' });
|
|
return;
|
|
}
|
|
|
|
const buffer = await data.toBuffer();
|
|
const csvContent = buffer.toString('utf-8');
|
|
|
|
const result = await this.importService.previewImport(csvContent);
|
|
reply.code(200).send(result);
|
|
} catch (error: any) {
|
|
logger.error('Error previewing import', { error });
|
|
reply.code(500).send({ error: error.message || 'Failed to preview import' });
|
|
}
|
|
}
|
|
|
|
async importApply(
|
|
request: FastifyRequest<{ Body: { previewId: string } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
if (!this.importService) {
|
|
reply.code(500).send({ error: 'Import service not configured' });
|
|
return;
|
|
}
|
|
|
|
const { previewId } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
if (!previewId) {
|
|
reply.code(400).send({ error: 'Preview ID is required' });
|
|
return;
|
|
}
|
|
|
|
const result = await this.importService.applyImport(previewId, actorId);
|
|
reply.code(200).send(result);
|
|
} catch (error: any) {
|
|
logger.error('Error applying import', { error });
|
|
if (error.message?.includes('expired') || error.message?.includes('not found')) {
|
|
reply.code(404).send({ error: error.message });
|
|
} else if (error.message?.includes('validation errors')) {
|
|
reply.code(400).send({ error: error.message });
|
|
} else {
|
|
reply.code(500).send({ error: error.message || 'Failed to apply import' });
|
|
}
|
|
}
|
|
}
|
|
|
|
async exportCatalog(
|
|
_request: FastifyRequest,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
if (!this.importService) {
|
|
reply.code(500).send({ error: 'Import service not configured' });
|
|
return;
|
|
}
|
|
|
|
const csvContent = await this.importService.exportCatalog();
|
|
|
|
reply
|
|
.header('Content-Type', 'text/csv')
|
|
.header('Content-Disposition', 'attachment; filename="vehicle-catalog.csv"')
|
|
.code(200)
|
|
.send(csvContent);
|
|
} catch (error: any) {
|
|
logger.error('Error exporting catalog', { error });
|
|
reply.code(500).send({ error: 'Failed to export catalog' });
|
|
}
|
|
}
|
|
|
|
// BULK DELETE ENDPOINT
|
|
|
|
async bulkDeleteCatalogEntity(
|
|
request: FastifyRequest<{ Params: { entity: string }; Body: { ids: number[] } }>,
|
|
reply: FastifyReply
|
|
): Promise<void> {
|
|
try {
|
|
const { entity } = request.params;
|
|
const { ids } = request.body;
|
|
const actorId = request.userContext?.userId || 'unknown';
|
|
|
|
// Validate entity type
|
|
const validEntities = ['makes', 'models', 'years', 'trims', 'engines'];
|
|
if (!validEntities.includes(entity)) {
|
|
reply.code(400).send({
|
|
error: 'Invalid entity type',
|
|
message: `Entity must be one of: ${validEntities.join(', ')}`
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validate IDs are provided
|
|
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
|
reply.code(400).send({
|
|
error: 'Invalid request',
|
|
message: 'At least one ID must be provided'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validate all IDs are valid integers
|
|
const invalidIds = ids.filter(id => !Number.isInteger(id) || id <= 0);
|
|
if (invalidIds.length > 0) {
|
|
reply.code(400).send({
|
|
error: 'Invalid IDs',
|
|
message: 'All IDs must be positive integers'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const deleted: number[] = [];
|
|
const failed: Array<{ id: number; error: string }> = [];
|
|
|
|
// Map entity to delete method
|
|
const deleteMethodMap: Record<string, (id: number, actorId: string) => Promise<void>> = {
|
|
makes: (id, actor) => this.catalogService.deleteMake(id, actor),
|
|
models: (id, actor) => this.catalogService.deleteModel(id, actor),
|
|
years: (id, actor) => this.catalogService.deleteYear(id, actor),
|
|
trims: (id, actor) => this.catalogService.deleteTrim(id, actor),
|
|
engines: (id, actor) => this.catalogService.deleteEngine(id, actor)
|
|
};
|
|
|
|
const deleteMethod = deleteMethodMap[entity];
|
|
|
|
// Process each deletion sequentially to maintain data consistency
|
|
for (const id of ids) {
|
|
try {
|
|
await deleteMethod(id, actorId);
|
|
deleted.push(id);
|
|
} catch (error: any) {
|
|
logger.error(`Error deleting ${entity} in bulk operation`, {
|
|
error: error.message,
|
|
entity,
|
|
id,
|
|
actorId
|
|
});
|
|
|
|
failed.push({
|
|
id,
|
|
error: error.message || `Failed to delete ${entity}`
|
|
});
|
|
}
|
|
}
|
|
|
|
const response = {
|
|
deleted,
|
|
failed
|
|
};
|
|
|
|
// Return 207 Multi-Status if there were any failures, 204 if all succeeded
|
|
if (failed.length > 0) {
|
|
reply.code(207).send(response);
|
|
} else {
|
|
reply.code(204).send();
|
|
}
|
|
} catch (error: any) {
|
|
logger.error('Error in bulk delete catalog entity', {
|
|
error: error.message,
|
|
entity: request.params.entity,
|
|
actorId: request.userContext?.userId
|
|
});
|
|
|
|
reply.code(500).send({
|
|
error: 'Internal server error',
|
|
message: 'Failed to process bulk deletion'
|
|
});
|
|
}
|
|
}
|
|
}
|