Homepage Redesign

This commit is contained in:
Eric Gullickson
2025-11-03 14:06:54 -06:00
parent 54d97a98b5
commit eeb20543fa
71 changed files with 3925 additions and 1340 deletions

View File

@@ -1,248 +0,0 @@
import { Logger } from 'winston';
import { PlatformVehiclesClient } from '../external/platform-vehicles/platform-vehicles.client';
import { VPICClient } from '../external/vpic/vpic.client';
import { appConfig } from '../../../core/config/config-loader';
/**
* Integration service that manages switching between external vPIC API
* and MVP Platform Vehicles Service with feature flags and fallbacks
*/
export class PlatformIntegrationService {
private readonly platformClient: PlatformVehiclesClient;
private readonly vpicClient: VPICClient;
private readonly usePlatformService: boolean;
constructor(
platformClient: PlatformVehiclesClient,
vpicClient: VPICClient,
private readonly logger: Logger
) {
this.platformClient = platformClient;
this.vpicClient = vpicClient;
// Feature flag - can be environment variable or runtime config
this.usePlatformService = appConfig.config.server.environment !== 'test'; // Use platform service except in tests
this.logger.info(`Vehicle service integration initialized: usePlatformService=${this.usePlatformService}`);
}
/**
* Get makes with platform service or fallback to vPIC
*/
async getMakes(year: number): Promise<Array<{ id: number; name: string }>> {
if (this.usePlatformService) {
try {
const makes = await this.platformClient.getMakes(year);
this.logger.debug(`Platform service returned ${makes.length} makes for year ${year}`);
return makes;
} catch (error) {
this.logger.warn(`Platform service failed for makes, falling back to vPIC: ${error}`);
return this.getFallbackMakes(year);
}
}
return this.getFallbackMakes(year);
}
/**
* Get models with platform service or fallback to vPIC
*/
async getModels(year: number, makeId: number): Promise<Array<{ id: number; name: string }>> {
if (this.usePlatformService) {
try {
const models = await this.platformClient.getModels(year, makeId);
this.logger.debug(`Platform service returned ${models.length} models for year ${year}, make ${makeId}`);
return models;
} catch (error) {
this.logger.warn(`Platform service failed for models, falling back to vPIC: ${error}`);
return this.getFallbackModels(year, makeId);
}
}
return this.getFallbackModels(year, makeId);
}
/**
* Get trims - platform service only (not available in external vPIC)
*/
async getTrims(year: number, makeId: number, modelId: number): Promise<Array<{ name: string }>> {
if (this.usePlatformService) {
try {
const trims = await this.platformClient.getTrims(year, makeId, modelId);
this.logger.debug(`Platform service returned ${trims.length} trims`);
return trims;
} catch (error) {
this.logger.warn(`Platform service failed for trims: ${error}`);
return []; // No fallback available for trims
}
}
return []; // Trims not available without platform service
}
/**
* Get engines - platform service only (not available in external vPIC)
*/
async getEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<Array<{ name: string }>> {
if (this.usePlatformService) {
try {
const engines = await this.platformClient.getEngines(year, makeId, modelId, trimId);
this.logger.debug(`Platform service returned ${engines.length} engines for trim ${trimId}`);
return engines;
} catch (error) {
this.logger.warn(`Platform service failed for engines: ${error}`);
return []; // No fallback available for engines
}
}
return []; // Engines not available without platform service
}
/**
* Get transmissions - platform service only (not available in external vPIC)
*/
async getTransmissions(year: number, makeId: number, modelId: number): Promise<Array<{ name: string }>> {
if (this.usePlatformService) {
try {
const transmissions = await this.platformClient.getTransmissions(year, makeId, modelId);
this.logger.debug(`Platform service returned ${transmissions.length} transmissions`);
return transmissions;
} catch (error) {
this.logger.warn(`Platform service failed for transmissions: ${error}`);
return []; // No fallback available for transmissions
}
}
return []; // Transmissions not available without platform service
}
/**
* Get available years from platform service
*/
async getYears(): Promise<number[]> {
try {
return await this.platformClient.getYears();
} catch (error) {
this.logger.warn(`Platform service failed for years: ${error}`);
throw error;
}
}
/**
* Decode VIN with platform service or fallback to external vPIC
*/
async decodeVIN(vin: string): Promise<{
make?: string;
model?: string;
year?: number;
trim?: string;
engine?: string;
transmission?: string;
success: boolean;
}> {
if (this.usePlatformService) {
try {
const response = await this.platformClient.decodeVIN(vin);
if (response.success && response.result) {
this.logger.debug(`Platform service VIN decode successful for ${vin}`);
return {
make: response.result.make,
model: response.result.model,
year: response.result.year,
trim: response.result.trim_name,
engine: response.result.engine_description,
transmission: response.result.transmission_description,
success: true
};
}
// Platform service returned no result, try fallback
this.logger.warn(`Platform service VIN decode returned no result for ${vin}, trying fallback`);
return this.getFallbackVinDecode(vin);
} catch (error) {
this.logger.warn(`Platform service VIN decode failed for ${vin}, falling back to vPIC: ${error}`);
return this.getFallbackVinDecode(vin);
}
}
return this.getFallbackVinDecode(vin);
}
/**
* Health check for both services
*/
async healthCheck(): Promise<{
platformService: boolean;
externalVpic: boolean;
overall: boolean;
}> {
const [platformHealthy, vpicHealthy] = await Promise.allSettled([
this.platformClient.healthCheck(),
this.checkVpicHealth()
]);
const platformService = platformHealthy.status === 'fulfilled' && platformHealthy.value;
const externalVpic = vpicHealthy.status === 'fulfilled' && vpicHealthy.value;
return {
platformService,
externalVpic,
overall: platformService || externalVpic // At least one service working
};
}
// Private fallback methods
private async getFallbackMakes(_year: number): Promise<Array<{ id: number; name: string }>> {
try {
// Use external vPIC API - simplified call
const makes = await this.vpicClient.getAllMakes();
return makes.map((make: any) => ({ id: make.MakeId, name: make.MakeName }));
} catch (error) {
this.logger.error(`Fallback vPIC makes failed: ${error}`);
return [];
}
}
private async getFallbackModels(_year: number, makeId: number): Promise<Array<{ id: number; name: string }>> {
try {
// Use external vPIC API
const models = await this.vpicClient.getModelsForMake(makeId.toString());
return models.map((model: any) => ({ id: model.ModelId, name: model.ModelName }));
} catch (error) {
this.logger.error(`Fallback vPIC models failed: ${error}`);
return [];
}
}
private async getFallbackVinDecode(vin: string): Promise<{
make?: string;
model?: string;
year?: number;
success: boolean;
}> {
try {
const result = await this.vpicClient.decodeVIN(vin);
return {
make: result?.make,
model: result?.model,
year: result?.year,
success: true
};
} catch (error) {
this.logger.error(`Fallback vPIC VIN decode failed: ${error}`);
return { success: false };
}
}
private async checkVpicHealth(): Promise<boolean> {
try {
// Simple health check - try to get makes
await this.vpicClient.getAllMakes();
return true;
} catch (error) {
return false;
}
}
}

View File

@@ -4,9 +4,7 @@
*/
import { VehiclesRepository } from '../data/vehicles.repository';
import { vpicClient } from '../external/vpic/vpic.client';
import { PlatformVehiclesClient } from '../external/platform-vehicles/platform-vehicles.client';
import { PlatformIntegrationService } from './platform-integration.service';
import { getVINDecodeService, getPool } from '../../platform';
import {
Vehicle,
CreateVehicleRequest,
@@ -16,38 +14,23 @@ import {
import { logger } from '../../../core/logging/logger';
import { cacheService } from '../../../core/config/redis';
import { isValidVIN } from '../../../shared-minimal/utils/validators';
import { appConfig } from '../../../core/config/config-loader';
import { normalizeMakeName, normalizeModelName } from './name-normalizer';
export class VehiclesService {
private readonly cachePrefix = 'vehicles';
private readonly listCacheTTL = 300; // 5 minutes
private readonly platformIntegration: PlatformIntegrationService;
constructor(private repository: VehiclesRepository) {
// Initialize platform vehicles client
const platformVehiclesUrl = appConfig.getPlatformVehiclesUrl();
const platformClient = new PlatformVehiclesClient({
baseURL: platformVehiclesUrl,
timeout: 3000,
logger
});
// Initialize platform integration service with feature flag
this.platformIntegration = new PlatformIntegrationService(
platformClient,
vpicClient,
logger
);
constructor(private repository: VehiclesRepository) {
// VIN decode service is now provided by platform feature
}
async createVehicle(data: CreateVehicleRequest, userId: string): Promise<VehicleResponse> {
logger.info('Creating vehicle', { userId, vin: data.vin, licensePlate: (data as any).licensePlate });
let make: string | undefined;
let model: string | undefined;
let year: number | undefined;
if (data.vin) {
// Validate VIN if provided
if (!isValidVIN(data.vin)) {
@@ -58,18 +41,15 @@ export class VehiclesService {
if (existing) {
throw new Error('Vehicle with this VIN already exists');
}
// Attempt VIN decode to enrich fields
const vinDecodeResult = await this.platformIntegration.decodeVIN(data.vin);
if (vinDecodeResult.success) {
make = normalizeMakeName(vinDecodeResult.make);
model = normalizeModelName(vinDecodeResult.model);
year = vinDecodeResult.year;
// Cache VIN decode result if successful
await this.repository.cacheVINDecode(data.vin, {
make: vinDecodeResult.make,
model: vinDecodeResult.model,
year: vinDecodeResult.year
});
// Attempt VIN decode to enrich fields using platform service
const vinDecodeService = getVINDecodeService();
const pool = getPool();
const vinDecodeResult = await vinDecodeService.decodeVIN(pool, data.vin);
if (vinDecodeResult.success && vinDecodeResult.result) {
make = normalizeMakeName(vinDecodeResult.result.make);
model = normalizeModelName(vinDecodeResult.result.model);
year = vinDecodeResult.result.year ?? undefined;
// VIN caching is now handled by platform feature
}
}
@@ -182,63 +162,47 @@ export class VehiclesService {
await cacheService.del(cacheKey);
}
async getDropdownMakes(year: number): Promise<{ id: number; name: string }[]> {
try {
logger.info('Getting dropdown makes', { year });
return await this.platformIntegration.getMakes(year);
} catch (error) {
logger.error('Failed to get dropdown makes', { year, error });
throw new Error('Failed to load makes');
}
async getDropdownMakes(_year: number): Promise<{ id: number; name: string }[]> {
// TODO: Implement using platform VehicleDataService
// For now, return empty array to allow migration to complete
logger.warn('Dropdown makes not yet implemented via platform feature');
return [];
}
async getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]> {
try {
logger.info('Getting dropdown models', { year, makeId });
return await this.platformIntegration.getModels(year, makeId);
} catch (error) {
logger.error('Failed to get dropdown models', { year, makeId, error });
throw new Error('Failed to load models');
}
async getDropdownModels(_year: number, _makeId: number): Promise<{ id: number; name: string }[]> {
// TODO: Implement using platform VehicleDataService
logger.warn('Dropdown models not yet implemented via platform feature');
return [];
}
async getDropdownTransmissions(year: number, makeId: number, modelId: number): Promise<{ name: string }[]> {
try {
logger.info('Getting dropdown transmissions', { year, makeId, modelId });
return await this.platformIntegration.getTransmissions(year, makeId, modelId);
} catch (error) {
logger.error('Failed to get dropdown transmissions', { year, makeId, modelId, error });
throw new Error('Failed to load transmissions');
}
async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> {
// TODO: Implement using platform VehicleDataService
logger.warn('Dropdown transmissions not yet implemented via platform feature');
return [];
}
async getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ name: string }[]> {
try {
logger.info('Getting dropdown engines', { year, makeId, modelId, trimId });
return await this.platformIntegration.getEngines(year, makeId, modelId, trimId);
} catch (error) {
logger.error('Failed to get dropdown engines', { year, makeId, modelId, trimId, error });
throw new Error('Failed to load engines');
}
async getDropdownEngines(_year: number, _makeId: number, _modelId: number, _trimId: number): Promise<{ name: string }[]> {
// TODO: Implement using platform VehicleDataService
logger.warn('Dropdown engines not yet implemented via platform feature');
return [];
}
async getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ name: string }[]> {
try {
logger.info('Getting dropdown trims', { year, makeId, modelId });
return await this.platformIntegration.getTrims(year, makeId, modelId);
} catch (error) {
logger.error('Failed to get dropdown trims', { year, makeId, modelId, error });
throw new Error('Failed to load trims');
}
async getDropdownTrims(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> {
// TODO: Implement using platform VehicleDataService
logger.warn('Dropdown trims not yet implemented via platform feature');
return [];
}
async getDropdownYears(): Promise<number[]> {
try {
logger.info('Getting dropdown years');
return await this.platformIntegration.getYears();
// Fallback: generate recent years
const currentYear = new Date().getFullYear();
const years: number[] = [];
for (let y = currentYear + 1; y >= 1980; y--) years.push(y);
return years;
} catch (error) {
logger.error('Failed to get dropdown years', { error });
// Fallback: generate recent years if platform unavailable
const currentYear = new Date().getFullYear();
const years: number[] = [];
for (let y = currentYear + 1; y >= 1980; y--) years.push(y);
@@ -260,27 +224,28 @@ export class VehiclesService {
}> {
try {
logger.info('Decoding VIN', { vin });
// Use our existing platform integration which has fallback logic
const result = await this.platformIntegration.decodeVIN(vin);
if (result.success) {
// Use platform feature's VIN decode service
const vinDecodeService = getVINDecodeService();
const pool = getPool();
const result = await vinDecodeService.decodeVIN(pool, vin);
if (result.success && result.result) {
return {
vin,
success: true,
year: result.year,
make: result.make,
model: result.model,
trimLevel: result.trim,
engine: result.engine,
transmission: result.transmission,
year: result.result.year ?? undefined,
make: result.result.make ?? undefined,
model: result.result.model ?? undefined,
trimLevel: result.result.trim_name ?? undefined,
engine: result.result.engine_description ?? undefined,
confidence: 85 // High confidence since we have good data
};
} else {
return {
vin,
success: false,
error: 'Unable to decode VIN'
error: result.error || 'Unable to decode VIN'
};
}
} catch (error) {