MVP Build
This commit is contained in:
160
backend/src/features/vehicles/domain/vehicles.service.ts
Normal file
160
backend/src/features/vehicles/domain/vehicles.service.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* @ai-summary Business logic for vehicles feature
|
||||
* @ai-context Handles VIN decoding, caching, and business rules
|
||||
*/
|
||||
|
||||
import { VehiclesRepository } from '../data/vehicles.repository';
|
||||
import { vpicClient } from '../external/vpic/vpic.client';
|
||||
import {
|
||||
Vehicle,
|
||||
CreateVehicleRequest,
|
||||
UpdateVehicleRequest,
|
||||
VehicleResponse
|
||||
} from './vehicles.types';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
import { cacheService } from '../../../core/config/redis';
|
||||
import { isValidVIN } from '../../../shared-minimal/utils/validators';
|
||||
|
||||
export class VehiclesService {
|
||||
private readonly cachePrefix = 'vehicles';
|
||||
private readonly listCacheTTL = 300; // 5 minutes
|
||||
|
||||
constructor(private repository: VehiclesRepository) {}
|
||||
|
||||
async createVehicle(data: CreateVehicleRequest, userId: string): Promise<VehicleResponse> {
|
||||
logger.info('Creating vehicle', { userId, vin: data.vin });
|
||||
|
||||
// Validate VIN
|
||||
if (!isValidVIN(data.vin)) {
|
||||
throw new Error('Invalid VIN format');
|
||||
}
|
||||
|
||||
// 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
|
||||
const vehicle = await this.repository.create({
|
||||
...data,
|
||||
userId,
|
||||
make: vinData?.make,
|
||||
model: vinData?.model,
|
||||
year: vinData?.year,
|
||||
});
|
||||
|
||||
// Cache VIN decode result
|
||||
if (vinData) {
|
||||
await this.repository.cacheVINDecode(data.vin, vinData);
|
||||
}
|
||||
|
||||
// Invalidate user's vehicle list cache
|
||||
await this.invalidateUserCache(userId);
|
||||
|
||||
return this.toResponse(vehicle);
|
||||
}
|
||||
|
||||
async getUserVehicles(userId: string): Promise<VehicleResponse[]> {
|
||||
const cacheKey = `${this.cachePrefix}:user:${userId}`;
|
||||
|
||||
// Check cache
|
||||
const cached = await cacheService.get<VehicleResponse[]>(cacheKey);
|
||||
if (cached) {
|
||||
logger.debug('Vehicle list cache hit', { userId });
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Get from database
|
||||
const vehicles = await this.repository.findByUserId(userId);
|
||||
const response = vehicles.map(v => this.toResponse(v));
|
||||
|
||||
// Cache result
|
||||
await cacheService.set(cacheKey, response, this.listCacheTTL);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async getVehicle(id: string, userId: string): Promise<VehicleResponse> {
|
||||
const vehicle = await this.repository.findById(id);
|
||||
|
||||
if (!vehicle) {
|
||||
throw new Error('Vehicle not found');
|
||||
}
|
||||
|
||||
if (vehicle.userId !== userId) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return this.toResponse(vehicle);
|
||||
}
|
||||
|
||||
async updateVehicle(
|
||||
id: string,
|
||||
data: UpdateVehicleRequest,
|
||||
userId: string
|
||||
): Promise<VehicleResponse> {
|
||||
// Verify ownership
|
||||
const existing = await this.repository.findById(id);
|
||||
if (!existing) {
|
||||
throw new Error('Vehicle not found');
|
||||
}
|
||||
if (existing.userId !== userId) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
// Update vehicle
|
||||
const updated = await this.repository.update(id, data);
|
||||
if (!updated) {
|
||||
throw new Error('Update failed');
|
||||
}
|
||||
|
||||
// Invalidate cache
|
||||
await this.invalidateUserCache(userId);
|
||||
|
||||
return this.toResponse(updated);
|
||||
}
|
||||
|
||||
async deleteVehicle(id: string, userId: string): Promise<void> {
|
||||
// Verify ownership
|
||||
const existing = await this.repository.findById(id);
|
||||
if (!existing) {
|
||||
throw new Error('Vehicle not found');
|
||||
}
|
||||
if (existing.userId !== userId) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
await this.repository.softDelete(id);
|
||||
|
||||
// Invalidate cache
|
||||
await this.invalidateUserCache(userId);
|
||||
}
|
||||
|
||||
private async invalidateUserCache(userId: string): Promise<void> {
|
||||
const cacheKey = `${this.cachePrefix}:user:${userId}`;
|
||||
await cacheService.del(cacheKey);
|
||||
}
|
||||
|
||||
private toResponse(vehicle: Vehicle): VehicleResponse {
|
||||
return {
|
||||
id: vehicle.id,
|
||||
userId: vehicle.userId,
|
||||
vin: vehicle.vin,
|
||||
make: vehicle.make,
|
||||
model: vehicle.model,
|
||||
year: vehicle.year,
|
||||
nickname: vehicle.nickname,
|
||||
color: vehicle.color,
|
||||
licensePlate: vehicle.licensePlate,
|
||||
odometerReading: vehicle.odometerReading,
|
||||
isActive: vehicle.isActive,
|
||||
createdAt: vehicle.createdAt.toISOString(),
|
||||
updatedAt: vehicle.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user