fix: before admin stations removal

This commit is contained in:
Eric Gullickson
2025-12-24 17:20:11 -06:00
parent 96ee43ea94
commit 8ef6b3d853
32 changed files with 1258 additions and 176 deletions

View File

@@ -50,7 +50,7 @@ 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 catalogImportService = new CatalogImportService(pool, platformCacheService);
const catalogController = new CatalogController(catalogService);
catalogController.setImportService(catalogImportService);

View File

@@ -5,9 +5,9 @@
import { Pool } from 'pg';
import { logger } from '../../../core/logging/logger';
import { PlatformCacheService } from '../../platform/domain/platform-cache.service';
export interface ImportRow {
action: 'add' | 'update' | 'delete';
year: number;
make: string;
model: string;
@@ -25,7 +25,6 @@ export interface ImportPreviewResult {
previewId: string;
toCreate: ImportRow[];
toUpdate: ImportRow[];
toDelete: ImportRow[];
errors: ImportError[];
valid: boolean;
}
@@ -33,7 +32,6 @@ export interface ImportPreviewResult {
export interface ImportApplyResult {
created: number;
updated: number;
deleted: number;
errors: ImportError[];
}
@@ -50,7 +48,10 @@ export interface ExportRow {
const previewCache = new Map<string, { data: ImportPreviewResult; expiresAt: number }>();
export class CatalogImportService {
constructor(private pool: Pool) {}
constructor(
private pool: Pool,
private platformCacheService?: PlatformCacheService
) {}
/**
* Parse CSV content and validate without applying changes
@@ -59,7 +60,6 @@ export class CatalogImportService {
const previewId = uuidv4();
const toCreate: ImportRow[] = [];
const toUpdate: ImportRow[] = [];
const toDelete: ImportRow[] = [];
const errors: ImportError[] = [];
const lines = csvContent.trim().split('\n');
@@ -68,7 +68,6 @@ export class CatalogImportService {
previewId,
toCreate,
toUpdate,
toDelete,
errors: [{ row: 0, error: 'CSV must have a header row and at least one data row' }],
valid: false,
};
@@ -78,15 +77,14 @@ export class CatalogImportService {
const header = this.parseCSVLine(lines[0]);
const headerLower = header.map(h => h.toLowerCase().trim());
// Validate required headers
const requiredHeaders = ['action', 'year', 'make', 'model', 'trim'];
// Validate required headers (no action column required - matches export format)
const requiredHeaders = ['year', 'make', 'model', 'trim'];
for (const required of requiredHeaders) {
if (!headerLower.includes(required)) {
return {
previewId,
toCreate,
toUpdate,
toDelete,
errors: [{ row: 1, error: `Missing required header: ${required}` }],
valid: false,
};
@@ -95,7 +93,6 @@ export class CatalogImportService {
// Find column indices
const colIndices = {
action: headerLower.indexOf('action'),
year: headerLower.indexOf('year'),
make: headerLower.indexOf('make'),
model: headerLower.indexOf('model'),
@@ -113,7 +110,6 @@ export class CatalogImportService {
const rowNum = i + 1;
try {
const action = values[colIndices.action]?.toLowerCase().trim();
const year = parseInt(values[colIndices.year], 10);
const make = values[colIndices.make]?.trim();
const model = values[colIndices.model]?.trim();
@@ -121,12 +117,6 @@ export class CatalogImportService {
const engineName = colIndices.engineName >= 0 ? values[colIndices.engineName]?.trim() || null : null;
const transmissionType = colIndices.transmissionType >= 0 ? values[colIndices.transmissionType]?.trim() || null : null;
// Validate action
if (!['add', 'update', 'delete'].includes(action)) {
errors.push({ row: rowNum, error: `Invalid action: ${action}. Must be add, update, or delete` });
continue;
}
// Validate year
if (isNaN(year) || year < 1900 || year > 2100) {
errors.push({ row: rowNum, error: `Invalid year: ${values[colIndices.year]}` });
@@ -148,7 +138,6 @@ export class CatalogImportService {
}
const row: ImportRow = {
action: action as 'add' | 'update' | 'delete',
year,
make,
model,
@@ -157,7 +146,7 @@ export class CatalogImportService {
transmissionType,
};
// Check if record exists for validation
// Check if record exists to determine create vs update (upsert logic)
const existsResult = await this.pool.query(
`SELECT id FROM vehicle_options
WHERE year = $1 AND make = $2 AND model = $3 AND trim = $4
@@ -166,26 +155,11 @@ export class CatalogImportService {
);
const exists = (existsResult.rowCount || 0) > 0;
if (action === 'add' && exists) {
errors.push({ row: rowNum, error: `Record already exists: ${year} ${make} ${model} ${trim}` });
continue;
}
if ((action === 'update' || action === 'delete') && !exists) {
errors.push({ row: rowNum, error: `Record not found: ${year} ${make} ${model} ${trim}` });
continue;
}
// Sort into appropriate bucket
switch (action) {
case 'add':
toCreate.push(row);
break;
case 'update':
toUpdate.push(row);
break;
case 'delete':
toDelete.push(row);
break;
// Auto-detect: if exists -> update, else -> create
if (exists) {
toUpdate.push(row);
} else {
toCreate.push(row);
}
} catch (error: any) {
errors.push({ row: rowNum, error: error.message || 'Parse error' });
@@ -196,7 +170,6 @@ export class CatalogImportService {
previewId,
toCreate,
toUpdate,
toDelete,
errors,
valid: errors.length === 0,
};
@@ -230,7 +203,6 @@ export class CatalogImportService {
const result: ImportApplyResult = {
created: 0,
updated: 0,
deleted: 0,
errors: [],
};
@@ -247,7 +219,7 @@ export class CatalogImportService {
const engineResult = await client.query(
`INSERT INTO engines (name, fuel_type)
VALUES ($1, 'Gas')
ON CONFLICT (LOWER(name)) DO UPDATE SET name = EXCLUDED.name
ON CONFLICT ((lower(name))) DO UPDATE SET name = EXCLUDED.name
RETURNING id`,
[row.engineName]
);
@@ -260,7 +232,7 @@ export class CatalogImportService {
const transResult = await client.query(
`INSERT INTO transmissions (type)
VALUES ($1)
ON CONFLICT (LOWER(type)) DO UPDATE SET type = EXCLUDED.type
ON CONFLICT ((lower(type))) DO UPDATE SET type = EXCLUDED.type
RETURNING id`,
[row.transmissionType]
);
@@ -289,7 +261,7 @@ export class CatalogImportService {
const engineResult = await client.query(
`INSERT INTO engines (name, fuel_type)
VALUES ($1, 'Gas')
ON CONFLICT (LOWER(name)) DO UPDATE SET name = EXCLUDED.name
ON CONFLICT ((lower(name))) DO UPDATE SET name = EXCLUDED.name
RETURNING id`,
[row.engineName]
);
@@ -302,7 +274,7 @@ export class CatalogImportService {
const transResult = await client.query(
`INSERT INTO transmissions (type)
VALUES ($1)
ON CONFLICT (LOWER(type)) DO UPDATE SET type = EXCLUDED.type
ON CONFLICT ((lower(type))) DO UPDATE SET type = EXCLUDED.type
RETURNING id`,
[row.transmissionType]
);
@@ -323,41 +295,21 @@ export class CatalogImportService {
}
}
// Process deletes
for (const row of preview.toDelete) {
try {
await client.query(
`DELETE FROM vehicle_options
WHERE year = $1 AND make = $2 AND model = $3 AND trim = $4`,
[row.year, row.make, row.model, row.trim]
);
result.deleted++;
} catch (error: any) {
result.errors.push({ row: 0, error: `Failed to delete ${row.year} ${row.make} ${row.model} ${row.trim}: ${error.message}` });
}
}
await client.query('COMMIT');
// Log the import action
await this.pool.query(
`INSERT INTO platform_change_log (change_type, resource_type, resource_id, old_value, new_value, changed_by)
VALUES ('CREATE', 'import', $1, NULL, $2, $3)`,
[
previewId,
JSON.stringify({ created: result.created, updated: result.updated, deleted: result.deleted }),
changedBy,
]
);
// Remove preview from cache
previewCache.delete(previewId);
// Invalidate vehicle data cache so dropdowns reflect new data
if (this.platformCacheService) {
await this.platformCacheService.invalidateVehicleData();
logger.debug('Vehicle data cache invalidated after import');
}
logger.info('Catalog import completed', {
previewId,
created: result.created,
updated: result.updated,
deleted: result.deleted,
errors: result.errors.length,
changedBy,
});