fix: preserve vehicle identity by checking ID first in merge mode (refs #26)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m21s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

Critical fix for merge mode vehicle matching logic.

Problem:
- Vehicles with same license plate but no VIN were matched to the same existing vehicle
- Example: 2 vehicles with license plate "TEST-123" both updated the same vehicle
- Result: "Updated: 2" but only 1 vehicle in database, second vehicle overwrites first

Root Cause:
- Matching order was: VIN → license plate
- Both vehicles had no VIN and same license plate
- Both matched the same existing vehicle by license plate

Solution:
- New matching order: ID → VIN → license plate
- Preserves vehicle identity across export/import cycles
- Vehicles exported with IDs will update the same vehicle on re-import
- New vehicles (no matching ID) will be created as new records
- Security check: Verify ID belongs to same user before matching

Benefits:
- Export-modify-import workflow now works correctly
- Vehicles maintain identity across imports
- Users can safely import data with duplicate license plates
- Prevents unintended overwrites

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-11 21:15:30 -06:00
parent 62b4dc31ab
commit 28574b0eb4

View File

@@ -246,23 +246,41 @@ export class UserImportService {
try { try {
logger.debug('Processing vehicle', { logger.debug('Processing vehicle', {
userId, userId,
id: vehicle.id,
vin: vehicle.vin, vin: vehicle.vin,
make: vehicle.make, make: vehicle.make,
model: vehicle.model, model: vehicle.model,
year: vehicle.year, year: vehicle.year,
licensePlate: vehicle.licensePlate,
}); });
let existing = null; let existing = null;
// Try to find existing vehicle by VIN first // Try to find existing vehicle by ID first (preserves identity across exports)
if (vehicle.vin && vehicle.vin.trim().length > 0) { if (vehicle.id) {
existing = await this.vehiclesRepo.findById(vehicle.id);
// Verify it belongs to the same user
if (existing && existing.userId !== userId) {
logger.warn('Vehicle ID belongs to different user, ignoring', {
vehicleId: vehicle.id,
expectedUserId: userId,
actualUserId: existing.userId,
});
existing = null;
} else if (existing) {
logger.debug('Found existing vehicle by ID', { vehicleId: existing.id });
}
}
// Try to find existing vehicle by VIN if not found by ID
if (!existing && vehicle.vin && vehicle.vin.trim().length > 0) {
existing = await this.vehiclesRepo.findByUserAndVIN(userId, vehicle.vin.trim()); existing = await this.vehiclesRepo.findByUserAndVIN(userId, vehicle.vin.trim());
if (existing) { if (existing) {
logger.debug('Found existing vehicle by VIN', { vehicleId: existing.id, vin: vehicle.vin }); logger.debug('Found existing vehicle by VIN', { vehicleId: existing.id, vin: vehicle.vin });
} }
} }
// If not found by VIN and license plate exists, try license plate // If not found by ID or VIN, and license plate exists, try license plate
if (!existing && vehicle.licensePlate && vehicle.licensePlate.trim().length > 0) { if (!existing && vehicle.licensePlate && vehicle.licensePlate.trim().length > 0) {
const allUserVehicles = await this.vehiclesRepo.findByUserId(userId); const allUserVehicles = await this.vehiclesRepo.findByUserId(userId);
existing = allUserVehicles.find( existing = allUserVehicles.find(