Vehicle ETL Process fixed. Admin settings fixed.
This commit is contained in:
@@ -20,6 +20,7 @@ import { StationOversightService } from '../domain/station-oversight.service';
|
||||
import { StationsController } from './stations.controller';
|
||||
import { CatalogController } from './catalog.controller';
|
||||
import { VehicleCatalogService } from '../domain/vehicle-catalog.service';
|
||||
import { CatalogImportService } from '../domain/catalog-import.service';
|
||||
import { PlatformCacheService } from '../../platform/domain/platform-cache.service';
|
||||
import { cacheService } from '../../../core/config/redis';
|
||||
import { pool } from '../../../core/config/database';
|
||||
@@ -35,7 +36,9 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
// Initialize catalog dependencies
|
||||
const platformCacheService = new PlatformCacheService(cacheService);
|
||||
const catalogService = new VehicleCatalogService(pool, platformCacheService);
|
||||
const catalogImportService = new CatalogImportService(pool);
|
||||
const catalogController = new CatalogController(catalogService);
|
||||
catalogController.setImportService(catalogImportService);
|
||||
|
||||
// Admin access verification (used by frontend auth checks)
|
||||
fastify.get('/admin/verify', {
|
||||
@@ -205,6 +208,49 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
handler: catalogController.getChangeLogs.bind(catalogController)
|
||||
});
|
||||
|
||||
// Search endpoint - full-text search across vehicle_options
|
||||
fastify.get('/admin/catalog/search', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: catalogController.searchCatalog.bind(catalogController)
|
||||
});
|
||||
|
||||
// Cascade delete endpoints - delete entity and all its children
|
||||
fastify.delete('/admin/catalog/makes/:makeId/cascade', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: catalogController.deleteMakeCascade.bind(catalogController)
|
||||
});
|
||||
|
||||
fastify.delete('/admin/catalog/models/:modelId/cascade', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: catalogController.deleteModelCascade.bind(catalogController)
|
||||
});
|
||||
|
||||
fastify.delete('/admin/catalog/years/:yearId/cascade', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: catalogController.deleteYearCascade.bind(catalogController)
|
||||
});
|
||||
|
||||
fastify.delete('/admin/catalog/trims/:trimId/cascade', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: catalogController.deleteTrimCascade.bind(catalogController)
|
||||
});
|
||||
|
||||
// Import/Export endpoints
|
||||
fastify.post('/admin/catalog/import/preview', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: catalogController.importPreview.bind(catalogController)
|
||||
});
|
||||
|
||||
fastify.post('/admin/catalog/import/apply', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: catalogController.importApply.bind(catalogController)
|
||||
});
|
||||
|
||||
fastify.get('/admin/catalog/export', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: catalogController.exportCatalog.bind(catalogController)
|
||||
});
|
||||
|
||||
// Bulk delete endpoint
|
||||
fastify.delete<{ Params: { entity: CatalogEntity }; Body: BulkDeleteCatalogInput }>('/admin/catalog/:entity/bulk-delete', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user