Initial Commit
This commit is contained in:
52
backend/src/features/vehicles/domain/name-normalizer.ts
Normal file
52
backend/src/features/vehicles/domain/name-normalizer.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Normalizes vehicle make and model names for human-friendly display.
|
||||
* - Replaces underscores with spaces
|
||||
* - Collapses whitespace
|
||||
* - Title-cases standard words
|
||||
* - Uppercases common acronyms (e.g., HD, GT, Z06)
|
||||
*/
|
||||
|
||||
const MODEL_ACRONYMS = new Set([
|
||||
'HD','GT','GL','SE','LE','XLE','RS','SVT','XR','ST','FX4','TRD','ZR1','Z06','GTI','GLI','SI','SS','LT','LTZ','RT','SRT','SR','SR5','XSE','SEL'
|
||||
]);
|
||||
|
||||
export function normalizeModelName(input?: string | null): string | undefined {
|
||||
if (input == null) return input ?? undefined;
|
||||
let s = String(input).replace(/_/g, ' ');
|
||||
s = s.replace(/\s+/g, ' ').trim();
|
||||
if (s.length === 0) return s;
|
||||
|
||||
const tokens = s.split(' ');
|
||||
const normalized = tokens.map(t => {
|
||||
const raw = t;
|
||||
const upper = raw.toUpperCase();
|
||||
const lower = raw.toLowerCase();
|
||||
// Uppercase known acronyms (match case-insensitively)
|
||||
if (MODEL_ACRONYMS.has(upper)) return upper;
|
||||
// Tokens with letters+digits (e.g., Z06) – prefer uppercase
|
||||
if (/^[a-z0-9]+$/i.test(raw) && /[a-z]/i.test(raw) && /\d/.test(raw) && raw.length <= 4) {
|
||||
return upper;
|
||||
}
|
||||
// Pure letters: title case
|
||||
if (/^[a-z]+$/i.test(raw)) {
|
||||
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
||||
}
|
||||
// Numbers or mixed/punctuated tokens: keep as-is except collapse case
|
||||
return raw;
|
||||
});
|
||||
return normalized.join(' ');
|
||||
}
|
||||
|
||||
export function normalizeMakeName(input?: string | null): string | undefined {
|
||||
if (input == null) return input ?? undefined;
|
||||
let s = String(input).replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (s.length === 0) return s;
|
||||
const title = s.toLowerCase().split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
||||
// Special cases
|
||||
if (/^bmw$/i.test(s)) return 'BMW';
|
||||
if (/^gmc$/i.test(s)) return 'GMC';
|
||||
if (/^mini$/i.test(s)) return 'MINI';
|
||||
if (/^mclaren$/i.test(s)) return 'McLaren';
|
||||
return title;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { Logger } from 'winston';
|
||||
import { PlatformVehiclesClient } from '../external/platform-vehicles/platform-vehicles.client';
|
||||
import { VPICClient } from '../external/vpic/vpic.client';
|
||||
import { env } from '../../../core/config/environment';
|
||||
|
||||
|
||||
/**
|
||||
* 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 = env.NODE_ENV !== '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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
export interface Vehicle {
|
||||
id: string;
|
||||
userId: string;
|
||||
vin: string;
|
||||
vin?: string;
|
||||
make?: string;
|
||||
model?: string;
|
||||
year?: number;
|
||||
@@ -26,7 +26,7 @@ export interface Vehicle {
|
||||
}
|
||||
|
||||
export interface CreateVehicleRequest {
|
||||
vin: string;
|
||||
vin?: string;
|
||||
make?: string;
|
||||
model?: string;
|
||||
engine?: string;
|
||||
@@ -57,7 +57,7 @@ export interface UpdateVehicleRequest {
|
||||
export interface VehicleResponse {
|
||||
id: string;
|
||||
userId: string;
|
||||
vin: string;
|
||||
vin?: string;
|
||||
make?: string;
|
||||
model?: string;
|
||||
year?: number;
|
||||
@@ -86,7 +86,7 @@ export interface VINDecodeResult {
|
||||
|
||||
// Fastify-specific types for HTTP handling
|
||||
export interface CreateVehicleBody {
|
||||
vin: string;
|
||||
vin?: string;
|
||||
nickname?: string;
|
||||
color?: string;
|
||||
licensePlate?: string;
|
||||
@@ -102,4 +102,4 @@ export interface UpdateVehicleBody {
|
||||
|
||||
export interface VehicleParams {
|
||||
id: string;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user