feat: Add user data import feature (Fixes #26) #27

Merged
egullickson merged 11 commits from issue-26-add-user-data-import into main 2026-01-12 03:22:32 +00:00
Showing only changes of commit f48a18287b - Show all commits

View File

@@ -9,6 +9,7 @@ import { Pool, PoolClient } from 'pg';
import { logger } from '../../../core/logging/logger'; import { logger } from '../../../core/logging/logger';
import { getStorageService } from '../../../core/storage/storage.service'; import { getStorageService } from '../../../core/storage/storage.service';
import { VehiclesRepository } from '../../vehicles/data/vehicles.repository'; import { VehiclesRepository } from '../../vehicles/data/vehicles.repository';
import { VehiclesService, VehicleLimitExceededError } from '../../vehicles/domain/vehicles.service';
import { FuelLogsRepository } from '../../fuel-logs/data/fuel-logs.repository'; import { FuelLogsRepository } from '../../fuel-logs/data/fuel-logs.repository';
import { DocumentsRepository } from '../../documents/data/documents.repository'; import { DocumentsRepository } from '../../documents/data/documents.repository';
import { MaintenanceRepository } from '../../maintenance/data/maintenance.repository'; import { MaintenanceRepository } from '../../maintenance/data/maintenance.repository';
@@ -18,6 +19,7 @@ import { ImportPreview, ImportResult, USER_IMPORT_CONFIG } from './user-import.t
export class UserImportService { export class UserImportService {
private readonly archiveService: UserImportArchiveService; private readonly archiveService: UserImportArchiveService;
private readonly vehiclesRepo: VehiclesRepository; private readonly vehiclesRepo: VehiclesRepository;
private readonly vehiclesService: VehiclesService;
private readonly fuelLogsRepo: FuelLogsRepository; private readonly fuelLogsRepo: FuelLogsRepository;
private readonly maintenanceRepo: MaintenanceRepository; private readonly maintenanceRepo: MaintenanceRepository;
private readonly documentsRepo: DocumentsRepository; private readonly documentsRepo: DocumentsRepository;
@@ -26,6 +28,7 @@ export class UserImportService {
constructor(private pool: Pool) { constructor(private pool: Pool) {
this.archiveService = new UserImportArchiveService(); this.archiveService = new UserImportArchiveService();
this.vehiclesRepo = new VehiclesRepository(pool); this.vehiclesRepo = new VehiclesRepository(pool);
this.vehiclesService = new VehiclesService(this.vehiclesRepo, pool);
this.fuelLogsRepo = new FuelLogsRepository(pool); this.fuelLogsRepo = new FuelLogsRepository(pool);
this.maintenanceRepo = new MaintenanceRepository(pool); this.maintenanceRepo = new MaintenanceRepository(pool);
this.documentsRepo = new DocumentsRepository(pool); this.documentsRepo = new DocumentsRepository(pool);
@@ -219,7 +222,7 @@ export class UserImportService {
} }
/** /**
* Merge vehicles: UPDATE existing by VIN, INSERT new * Merge vehicles: UPDATE existing by VIN or license plate, INSERT new with limit enforcement
*/ */
private async mergeVehicles( private async mergeVehicles(
userId: string, userId: string,
@@ -239,9 +242,20 @@ export class UserImportService {
for (const vehicle of chunk) { for (const vehicle of chunk) {
try { try {
// Check if vehicle exists by VIN let existing = null;
// Try to find existing vehicle by VIN first
if (vehicle.vin && vehicle.vin.trim().length > 0) { if (vehicle.vin && vehicle.vin.trim().length > 0) {
const existing = await this.vehiclesRepo.findByUserAndVIN(userId, vehicle.vin.trim()); existing = await this.vehiclesRepo.findByUserAndVIN(userId, vehicle.vin.trim());
}
// If not found by VIN and license plate exists, try license plate
if (!existing && vehicle.licensePlate && vehicle.licensePlate.trim().length > 0) {
const allUserVehicles = await this.vehiclesRepo.findByUserId(userId);
existing = allUserVehicles.find(
(v) => v.licensePlate && v.licensePlate.toLowerCase() === vehicle.licensePlate.toLowerCase()
) || null;
}
if (existing) { if (existing) {
// Update existing vehicle // Update existing vehicle
@@ -262,20 +276,39 @@ export class UserImportService {
summary.updated++; summary.updated++;
continue; continue;
} }
}
// Insert new vehicle // Insert new vehicle using service (enforces tier limits)
await this.vehiclesRepo.create({ await this.vehiclesService.createVehicle(
...vehicle, {
userId, vin: vehicle.vin || '',
}); make: vehicle.make,
model: vehicle.model,
year: vehicle.year,
engine: vehicle.engine,
transmission: vehicle.transmission,
trimLevel: vehicle.trimLevel,
driveType: vehicle.driveType,
fuelType: vehicle.fuelType,
nickname: vehicle.nickname,
color: vehicle.color,
licensePlate: vehicle.licensePlate,
odometerReading: vehicle.odometerReading,
},
userId
);
summary.imported++; summary.imported++;
} catch (error) { } catch (error) {
if (error instanceof VehicleLimitExceededError) {
summary.errors.push(
`Vehicle limit exceeded: ${error.upgradePrompt} (current: ${error.currentCount}/${error.limit})`
);
} else {
summary.errors.push(`Vehicle import failed: ${error instanceof Error ? error.message : String(error)}`); summary.errors.push(`Vehicle import failed: ${error instanceof Error ? error.message : String(error)}`);
} }
} }
} }
} }
}
/** /**
* Merge fuel logs: batch insert new records * Merge fuel logs: batch insert new records