Vehicle ETL Process fixed. Admin settings fixed.

This commit is contained in:
Eric Gullickson
2025-12-15 20:51:52 -06:00
parent 1a9ead9d9d
commit b84d4c7fef
23 changed files with 4553 additions and 2450 deletions

View File

@@ -5,11 +5,18 @@
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> {
@@ -593,6 +600,221 @@ export class CatalogController {
}
}
// 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(