Initial Commit

This commit is contained in:
Eric Gullickson
2025-09-17 16:09:15 -05:00
parent 0cdb9803de
commit a052040e3a
373 changed files with 437090 additions and 6773 deletions

View File

@@ -5,6 +5,8 @@
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 {
Vehicle,
CreateVehicleRequest,
@@ -14,44 +16,76 @@ import {
import { logger } from '../../../core/logging/logger';
import { cacheService } from '../../../core/config/redis';
import { isValidVIN } from '../../../shared-minimal/utils/validators';
import { env } from '../../../core/config/environment';
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) {}
constructor(private repository: VehiclesRepository) {
// Initialize platform vehicles client
const platformClient = new PlatformVehiclesClient({
baseURL: env.PLATFORM_VEHICLES_API_URL,
apiKey: env.PLATFORM_VEHICLES_API_KEY,
tenantId: process.env.TENANT_ID,
timeout: 3000,
logger
});
// Initialize platform integration service with feature flag
this.platformIntegration = new PlatformIntegrationService(
platformClient,
vpicClient,
logger
);
}
async createVehicle(data: CreateVehicleRequest, userId: string): Promise<VehicleResponse> {
logger.info('Creating vehicle', { userId, vin: data.vin });
logger.info('Creating vehicle', { userId, vin: data.vin, licensePlate: (data as any).licensePlate });
// Validate VIN
if (!isValidVIN(data.vin)) {
throw new Error('Invalid VIN format');
let make: string | undefined;
let model: string | undefined;
let year: number | undefined;
if (data.vin) {
// Validate VIN if provided
if (!isValidVIN(data.vin)) {
throw new Error('Invalid VIN format');
}
// Duplicate check only when VIN is present
const existing = await this.repository.findByUserAndVIN(userId, data.vin);
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
});
}
}
// Check for duplicate
const existing = await this.repository.findByUserAndVIN(userId, data.vin);
if (existing) {
throw new Error('Vehicle with this VIN already exists');
}
// Decode VIN
const vinData = await vpicClient.decodeVIN(data.vin);
// Create vehicle with decoded data
// Create vehicle (VIN optional). Client-sent make/model/year override decode if provided.
const inputMake = (data as any).make ?? make;
const inputModel = (data as any).model ?? model;
const vehicle = await this.repository.create({
...data,
userId,
make: vinData?.make,
model: vinData?.model,
year: vinData?.year,
make: normalizeMakeName(inputMake),
model: normalizeModelName(inputModel),
year: (data as any).year ?? year,
});
// Cache VIN decode result
if (vinData) {
await this.repository.cacheVINDecode(data.vin, vinData);
}
// Invalidate user's vehicle list cache
await this.invalidateUserCache(userId);
@@ -106,8 +140,17 @@ export class VehiclesService {
throw new Error('Unauthorized');
}
// Normalize any provided name fields
const normalized: UpdateVehicleRequest = { ...data } as any;
if (data.make !== undefined) {
(normalized as any).make = normalizeMakeName(data.make);
}
if (data.model !== undefined) {
(normalized as any).model = normalizeModelName(data.model);
}
// Update vehicle
const updated = await this.repository.update(id, data);
const updated = await this.repository.update(id, normalized);
if (!updated) {
throw new Error('Update failed');
}
@@ -140,81 +183,117 @@ export class VehiclesService {
await cacheService.del(cacheKey);
}
async getDropdownMakes(): Promise<{ id: number; name: string }[]> {
async getDropdownMakes(year: number): Promise<{ id: number; name: string }[]> {
try {
logger.info('Getting dropdown makes');
const makes = await vpicClient.getAllMakes();
return makes.map(make => ({
id: make.Make_ID,
name: make.Make_Name
}));
logger.info('Getting dropdown makes', { year });
return await this.platformIntegration.getMakes(year);
} catch (error) {
logger.error('Failed to get dropdown makes', { error });
logger.error('Failed to get dropdown makes', { year, error });
throw new Error('Failed to load makes');
}
}
async getDropdownModels(make: string): Promise<{ id: number; name: string }[]> {
async getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]> {
try {
logger.info('Getting dropdown models', { make });
const models = await vpicClient.getModelsForMake(make);
return models.map(model => ({
id: model.Model_ID,
name: model.Model_Name
}));
logger.info('Getting dropdown models', { year, makeId });
return await this.platformIntegration.getModels(year, makeId);
} catch (error) {
logger.error('Failed to get dropdown models', { make, error });
logger.error('Failed to get dropdown models', { year, makeId, error });
throw new Error('Failed to load models');
}
}
async getDropdownTransmissions(): Promise<{ id: number; name: string }[]> {
async getDropdownTransmissions(year: number, makeId: number, modelId: number): Promise<{ name: string }[]> {
try {
logger.info('Getting dropdown transmissions');
const transmissions = await vpicClient.getTransmissionTypes();
return transmissions.map(transmission => ({
id: transmission.Id,
name: transmission.Name
}));
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', { error });
logger.error('Failed to get dropdown transmissions', { year, makeId, modelId, error });
throw new Error('Failed to load transmissions');
}
}
async getDropdownEngines(): Promise<{ id: number; name: string }[]> {
async getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ name: string }[]> {
try {
logger.info('Getting dropdown engines');
const engines = await vpicClient.getEngineConfigurations();
return engines.map(engine => ({
id: engine.Id,
name: engine.Name
}));
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', { error });
logger.error('Failed to get dropdown engines', { year, makeId, modelId, trimId, error });
throw new Error('Failed to load engines');
}
}
async getDropdownTrims(): Promise<{ id: number; name: string }[]> {
async getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ name: string }[]> {
try {
logger.info('Getting dropdown trims');
const trims = await vpicClient.getTrimLevels();
return trims.map(trim => ({
id: trim.Id,
name: trim.Name
}));
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', { error });
logger.error('Failed to get dropdown trims', { year, makeId, modelId, error });
throw new Error('Failed to load trims');
}
}
async getDropdownYears(): Promise<number[]> {
try {
logger.info('Getting dropdown years');
return await this.platformIntegration.getYears();
} 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);
return years;
}
}
async decodeVIN(vin: string): Promise<{
vin: string;
success: boolean;
year?: number;
make?: string;
model?: string;
trimLevel?: string;
engine?: string;
transmission?: string;
confidence?: number;
error?: string;
}> {
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) {
return {
vin,
success: true,
year: result.year,
make: result.make,
model: result.model,
trimLevel: result.trim,
engine: result.engine,
transmission: result.transmission,
confidence: 85 // High confidence since we have good data
};
} else {
return {
vin,
success: false,
error: 'Unable to decode VIN'
};
}
} catch (error) {
logger.error('Failed to decode VIN', { vin, error });
return {
vin,
success: false,
error: 'VIN decode service unavailable'
};
}
}
private toResponse(vehicle: Vehicle): VehicleResponse {
return {
id: vehicle.id,
@@ -237,4 +316,4 @@ export class VehiclesService {
updatedAt: vehicle.updatedAt.toISOString(),
};
}
}
}