Admin User v1
This commit is contained in:
975
backend/src/features/admin/domain/vehicle-catalog.service.ts
Normal file
975
backend/src/features/admin/domain/vehicle-catalog.service.ts
Normal file
@@ -0,0 +1,975 @@
|
||||
/**
|
||||
* @ai-summary Vehicle catalog management service
|
||||
* @ai-context Handles CRUD operations on platform vehicle catalog data with transaction support
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
import { PlatformCacheService } from '../../platform/domain/platform-cache.service';
|
||||
|
||||
export interface CatalogMake {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CatalogModel {
|
||||
id: number;
|
||||
makeId: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CatalogYear {
|
||||
id: number;
|
||||
modelId: number;
|
||||
year: number;
|
||||
}
|
||||
|
||||
export interface CatalogTrim {
|
||||
id: number;
|
||||
yearId: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CatalogEngine {
|
||||
id: number;
|
||||
trimId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface PlatformChangeLog {
|
||||
id: string;
|
||||
changeType: 'CREATE' | 'UPDATE' | 'DELETE';
|
||||
resourceType: 'makes' | 'models' | 'years' | 'trims' | 'engines';
|
||||
resourceId: string;
|
||||
oldValue: any;
|
||||
newValue: any;
|
||||
changedBy: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class VehicleCatalogService {
|
||||
constructor(
|
||||
private pool: Pool,
|
||||
private cacheService: PlatformCacheService
|
||||
) {}
|
||||
|
||||
// MAKES OPERATIONS
|
||||
|
||||
async getAllMakes(): Promise<CatalogMake[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:makes:%'
|
||||
ORDER BY (data->>'name')
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query);
|
||||
return result.rows.map(row => ({
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
name: row.data.name
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting all makes', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createMake(name: string, changedBy: string): Promise<CatalogMake> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Get next ID
|
||||
const idResult = await client.query(`
|
||||
SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:makes:%'
|
||||
`);
|
||||
const makeId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert make
|
||||
const make: CatalogMake = { id: makeId, name };
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
`, [`catalog:makes:${makeId}`, JSON.stringify(make)]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'CREATE', 'makes', makeId.toString(), null, make, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Make created', { makeId, name, changedBy });
|
||||
return make;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error creating make', { error, name });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async updateMake(makeId: number, name: string, changedBy: string): Promise<CatalogMake> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:makes:${makeId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Make ${makeId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogMake = { id: makeId, name };
|
||||
|
||||
// Update make
|
||||
await client.query(`
|
||||
UPDATE vehicle_dropdown_cache
|
||||
SET data = $1, updated_at = NOW()
|
||||
WHERE cache_key = $2
|
||||
`, [JSON.stringify(newValue), `catalog:makes:${makeId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'UPDATE', 'makes', makeId.toString(), oldValue, newValue, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Make updated', { makeId, name, changedBy });
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error updating make', { error, makeId, name });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMake(makeId: number, changedBy: string): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Check for dependent models
|
||||
const modelsCheck = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:models:%'
|
||||
AND (data->>'makeId')::int = $1
|
||||
`, [makeId]);
|
||||
|
||||
if (parseInt(modelsCheck.rows[0].count) > 0) {
|
||||
throw new Error('Cannot delete make with existing models');
|
||||
}
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:makes:${makeId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Make ${makeId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
|
||||
// Delete make
|
||||
await client.query(`
|
||||
DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:makes:${makeId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'DELETE', 'makes', makeId.toString(), oldValue, null, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Make deleted', { makeId, changedBy });
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error deleting make', { error, makeId });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// MODELS OPERATIONS
|
||||
|
||||
async getModelsByMake(makeId: number): Promise<CatalogModel[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:models:%'
|
||||
AND (data->>'makeId')::int = $1
|
||||
ORDER BY (data->>'name')
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query, [makeId]);
|
||||
return result.rows.map(row => ({
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
makeId: row.data.makeId,
|
||||
name: row.data.name
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting models by make', { error, makeId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createModel(makeId: number, name: string, changedBy: string): Promise<CatalogModel> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Verify make exists
|
||||
const makeCheck = await client.query(`
|
||||
SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:makes:${makeId}`]);
|
||||
|
||||
if (makeCheck.rows.length === 0) {
|
||||
throw new Error(`Make ${makeId} not found`);
|
||||
}
|
||||
|
||||
// Get next ID
|
||||
const idResult = await client.query(`
|
||||
SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:models:%'
|
||||
`);
|
||||
const modelId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert model
|
||||
const model: CatalogModel = { id: modelId, makeId, name };
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
`, [`catalog:models:${modelId}`, JSON.stringify(model)]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'CREATE', 'models', modelId.toString(), null, model, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Model created', { modelId, makeId, name, changedBy });
|
||||
return model;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error creating model', { error, makeId, name });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async updateModel(modelId: number, makeId: number, name: string, changedBy: string): Promise<CatalogModel> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Verify make exists
|
||||
const makeCheck = await client.query(`
|
||||
SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:makes:${makeId}`]);
|
||||
|
||||
if (makeCheck.rows.length === 0) {
|
||||
throw new Error(`Make ${makeId} not found`);
|
||||
}
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:models:${modelId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Model ${modelId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogModel = { id: modelId, makeId, name };
|
||||
|
||||
// Update model
|
||||
await client.query(`
|
||||
UPDATE vehicle_dropdown_cache
|
||||
SET data = $1, updated_at = NOW()
|
||||
WHERE cache_key = $2
|
||||
`, [JSON.stringify(newValue), `catalog:models:${modelId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'UPDATE', 'models', modelId.toString(), oldValue, newValue, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Model updated', { modelId, makeId, name, changedBy });
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error updating model', { error, modelId, name });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteModel(modelId: number, changedBy: string): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Check for dependent years
|
||||
const yearsCheck = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:years:%'
|
||||
AND (data->>'modelId')::int = $1
|
||||
`, [modelId]);
|
||||
|
||||
if (parseInt(yearsCheck.rows[0].count) > 0) {
|
||||
throw new Error('Cannot delete model with existing years');
|
||||
}
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:models:${modelId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Model ${modelId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
|
||||
// Delete model
|
||||
await client.query(`
|
||||
DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:models:${modelId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'DELETE', 'models', modelId.toString(), oldValue, null, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Model deleted', { modelId, changedBy });
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error deleting model', { error, modelId });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// YEARS OPERATIONS
|
||||
|
||||
async getYearsByModel(modelId: number): Promise<CatalogYear[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:years:%'
|
||||
AND (data->>'modelId')::int = $1
|
||||
ORDER BY (data->>'year')::int DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query, [modelId]);
|
||||
return result.rows.map(row => ({
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
modelId: row.data.modelId,
|
||||
year: row.data.year
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting years by model', { error, modelId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createYear(modelId: number, year: number, changedBy: string): Promise<CatalogYear> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Verify model exists
|
||||
const modelCheck = await client.query(`
|
||||
SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:models:${modelId}`]);
|
||||
|
||||
if (modelCheck.rows.length === 0) {
|
||||
throw new Error(`Model ${modelId} not found`);
|
||||
}
|
||||
|
||||
// Get next ID
|
||||
const idResult = await client.query(`
|
||||
SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:years:%'
|
||||
`);
|
||||
const yearId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert year
|
||||
const yearData: CatalogYear = { id: yearId, modelId, year };
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
`, [`catalog:years:${yearId}`, JSON.stringify(yearData)]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'CREATE', 'years', yearId.toString(), null, yearData, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Year created', { yearId, modelId, year, changedBy });
|
||||
return yearData;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error creating year', { error, modelId, year });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async updateYear(yearId: number, modelId: number, year: number, changedBy: string): Promise<CatalogYear> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Verify model exists
|
||||
const modelCheck = await client.query(`
|
||||
SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:models:${modelId}`]);
|
||||
|
||||
if (modelCheck.rows.length === 0) {
|
||||
throw new Error(`Model ${modelId} not found`);
|
||||
}
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:years:${yearId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Year ${yearId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogYear = { id: yearId, modelId, year };
|
||||
|
||||
// Update year
|
||||
await client.query(`
|
||||
UPDATE vehicle_dropdown_cache
|
||||
SET data = $1, updated_at = NOW()
|
||||
WHERE cache_key = $2
|
||||
`, [JSON.stringify(newValue), `catalog:years:${yearId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'UPDATE', 'years', yearId.toString(), oldValue, newValue, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Year updated', { yearId, modelId, year, changedBy });
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error updating year', { error, yearId, year });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteYear(yearId: number, changedBy: string): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Check for dependent trims
|
||||
const trimsCheck = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:trims:%'
|
||||
AND (data->>'yearId')::int = $1
|
||||
`, [yearId]);
|
||||
|
||||
if (parseInt(trimsCheck.rows[0].count) > 0) {
|
||||
throw new Error('Cannot delete year with existing trims');
|
||||
}
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:years:${yearId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Year ${yearId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
|
||||
// Delete year
|
||||
await client.query(`
|
||||
DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:years:${yearId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'DELETE', 'years', yearId.toString(), oldValue, null, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Year deleted', { yearId, changedBy });
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error deleting year', { error, yearId });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// TRIMS OPERATIONS
|
||||
|
||||
async getTrimsByYear(yearId: number): Promise<CatalogTrim[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:trims:%'
|
||||
AND (data->>'yearId')::int = $1
|
||||
ORDER BY (data->>'name')
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query, [yearId]);
|
||||
return result.rows.map(row => ({
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
yearId: row.data.yearId,
|
||||
name: row.data.name
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting trims by year', { error, yearId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createTrim(yearId: number, name: string, changedBy: string): Promise<CatalogTrim> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Verify year exists
|
||||
const yearCheck = await client.query(`
|
||||
SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:years:${yearId}`]);
|
||||
|
||||
if (yearCheck.rows.length === 0) {
|
||||
throw new Error(`Year ${yearId} not found`);
|
||||
}
|
||||
|
||||
// Get next ID
|
||||
const idResult = await client.query(`
|
||||
SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:trims:%'
|
||||
`);
|
||||
const trimId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert trim
|
||||
const trim: CatalogTrim = { id: trimId, yearId, name };
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
`, [`catalog:trims:${trimId}`, JSON.stringify(trim)]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'CREATE', 'trims', trimId.toString(), null, trim, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Trim created', { trimId, yearId, name, changedBy });
|
||||
return trim;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error creating trim', { error, yearId, name });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async updateTrim(trimId: number, yearId: number, name: string, changedBy: string): Promise<CatalogTrim> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Verify year exists
|
||||
const yearCheck = await client.query(`
|
||||
SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:years:${yearId}`]);
|
||||
|
||||
if (yearCheck.rows.length === 0) {
|
||||
throw new Error(`Year ${yearId} not found`);
|
||||
}
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:trims:${trimId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Trim ${trimId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogTrim = { id: trimId, yearId, name };
|
||||
|
||||
// Update trim
|
||||
await client.query(`
|
||||
UPDATE vehicle_dropdown_cache
|
||||
SET data = $1, updated_at = NOW()
|
||||
WHERE cache_key = $2
|
||||
`, [JSON.stringify(newValue), `catalog:trims:${trimId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'UPDATE', 'trims', trimId.toString(), oldValue, newValue, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Trim updated', { trimId, yearId, name, changedBy });
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error updating trim', { error, trimId, name });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteTrim(trimId: number, changedBy: string): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Check for dependent engines
|
||||
const enginesCheck = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:engines:%'
|
||||
AND (data->>'trimId')::int = $1
|
||||
`, [trimId]);
|
||||
|
||||
if (parseInt(enginesCheck.rows[0].count) > 0) {
|
||||
throw new Error('Cannot delete trim with existing engines');
|
||||
}
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:trims:${trimId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Trim ${trimId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
|
||||
// Delete trim
|
||||
await client.query(`
|
||||
DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:trims:${trimId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'DELETE', 'trims', trimId.toString(), oldValue, null, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Trim deleted', { trimId, changedBy });
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error deleting trim', { error, trimId });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ENGINES OPERATIONS
|
||||
|
||||
async getEnginesByTrim(trimId: number): Promise<CatalogEngine[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:engines:%'
|
||||
AND (data->>'trimId')::int = $1
|
||||
ORDER BY (data->>'name')
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query, [trimId]);
|
||||
return result.rows.map(row => ({
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
trimId: row.data.trimId,
|
||||
name: row.data.name,
|
||||
description: row.data.description
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting engines by trim', { error, trimId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createEngine(trimId: number, name: string, description: string | undefined, changedBy: string): Promise<CatalogEngine> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Verify trim exists
|
||||
const trimCheck = await client.query(`
|
||||
SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:trims:${trimId}`]);
|
||||
|
||||
if (trimCheck.rows.length === 0) {
|
||||
throw new Error(`Trim ${trimId} not found`);
|
||||
}
|
||||
|
||||
// Get next ID
|
||||
const idResult = await client.query(`
|
||||
SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:engines:%'
|
||||
`);
|
||||
const engineId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert engine
|
||||
const engine: CatalogEngine = { id: engineId, trimId, name, description };
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
`, [`catalog:engines:${engineId}`, JSON.stringify(engine)]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'CREATE', 'engines', engineId.toString(), null, engine, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Engine created', { engineId, trimId, name, changedBy });
|
||||
return engine;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error creating engine', { error, trimId, name });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async updateEngine(engineId: number, trimId: number, name: string, description: string | undefined, changedBy: string): Promise<CatalogEngine> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Verify trim exists
|
||||
const trimCheck = await client.query(`
|
||||
SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:trims:${trimId}`]);
|
||||
|
||||
if (trimCheck.rows.length === 0) {
|
||||
throw new Error(`Trim ${trimId} not found`);
|
||||
}
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:engines:${engineId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Engine ${engineId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogEngine = { id: engineId, trimId, name, description };
|
||||
|
||||
// Update engine
|
||||
await client.query(`
|
||||
UPDATE vehicle_dropdown_cache
|
||||
SET data = $1, updated_at = NOW()
|
||||
WHERE cache_key = $2
|
||||
`, [JSON.stringify(newValue), `catalog:engines:${engineId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'UPDATE', 'engines', engineId.toString(), oldValue, newValue, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Engine updated', { engineId, trimId, name, changedBy });
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error updating engine', { error, engineId, name });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEngine(engineId: number, changedBy: string): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:engines:${engineId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Engine ${engineId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
|
||||
// Delete engine
|
||||
await client.query(`
|
||||
DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:engines:${engineId}`]);
|
||||
|
||||
// Log change
|
||||
await this.logChange(client, 'DELETE', 'engines', engineId.toString(), oldValue, null, changedBy);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Invalidate cache
|
||||
await this.cacheService.invalidateVehicleData();
|
||||
|
||||
logger.info('Engine deleted', { engineId, changedBy });
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error deleting engine', { error, engineId });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// HELPER METHODS
|
||||
|
||||
private async logChange(
|
||||
client: any,
|
||||
changeType: 'CREATE' | 'UPDATE' | 'DELETE',
|
||||
resourceType: 'makes' | 'models' | 'years' | 'trims' | 'engines',
|
||||
resourceId: string,
|
||||
oldValue: any,
|
||||
newValue: any,
|
||||
changedBy: string
|
||||
): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO platform_change_log (change_type, resource_type, resource_id, old_value, new_value, changed_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`;
|
||||
|
||||
await client.query(query, [
|
||||
changeType,
|
||||
resourceType,
|
||||
resourceId,
|
||||
oldValue ? JSON.stringify(oldValue) : null,
|
||||
newValue ? JSON.stringify(newValue) : null,
|
||||
changedBy
|
||||
]);
|
||||
}
|
||||
|
||||
async getChangeLogs(limit: number = 100, offset: number = 0): Promise<{ logs: PlatformChangeLog[]; total: number }> {
|
||||
const countQuery = 'SELECT COUNT(*) as total FROM platform_change_log';
|
||||
const query = `
|
||||
SELECT id, change_type, resource_type, resource_id, old_value, new_value, changed_by, created_at
|
||||
FROM platform_change_log
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`;
|
||||
|
||||
try {
|
||||
const [countResult, dataResult] = await Promise.all([
|
||||
this.pool.query(countQuery),
|
||||
this.pool.query(query, [limit, offset])
|
||||
]);
|
||||
|
||||
const total = parseInt(countResult.rows[0].total, 10);
|
||||
const logs = dataResult.rows.map(row => ({
|
||||
id: row.id,
|
||||
changeType: row.change_type,
|
||||
resourceType: row.resource_type,
|
||||
resourceId: row.resource_id,
|
||||
oldValue: row.old_value,
|
||||
newValue: row.new_value,
|
||||
changedBy: row.changed_by,
|
||||
createdAt: new Date(row.created_at)
|
||||
}));
|
||||
|
||||
return { logs, total };
|
||||
} catch (error) {
|
||||
logger.error('Error fetching change logs', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user