Align driver card identity normalization

This commit is contained in:
trifonovt 2026-05-21 14:15:17 +02:00
parent 33e9cb62c3
commit 4535f620fc
24 changed files with 297 additions and 52 deletions

View File

@ -1,5 +1,6 @@
package at.procon.eventhub.dto; package at.procon.eventhub.dto;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import at.procon.eventhub.reference.TachographNationRegistry; import at.procon.eventhub.reference.TachographNationRegistry;
/** /**
@ -43,6 +44,6 @@ public record DriverCardRefDto(
} }
private static String normalizeNullable(String value) { private static String normalizeNullable(String value) {
return value == null || value.isBlank() ? null : value.trim(); return DriverCardNumberNormalizer.canonical(value);
} }
} }

View File

@ -2,6 +2,7 @@ package at.procon.eventhub.persistence;
import at.procon.eventhub.dto.DriverCardRefDto; import at.procon.eventhub.dto.DriverCardRefDto;
import at.procon.eventhub.dto.DriverRefDto; import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDate; import java.time.LocalDate;
@ -16,9 +17,6 @@ import org.springframework.transaction.annotation.Transactional;
@Repository @Repository
public class DriverIdentityRepository { public class DriverIdentityRepository {
private static final String YELLOWFOX_SYNTHETIC_REFERENCE_NATION = "YELLOWFOX";
private static final String UNKNOWN_CARD_NATION = "UNKNOWN";
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@ -41,7 +39,7 @@ public class DriverIdentityRepository {
DriverCardRefDto driverCard = driverRef.driverCard(); DriverCardRefDto driverCard = driverRef.driverCard();
String cardNation = driverCard == null ? null : normalizeNullable(driverCard.nation()); String cardNation = driverCard == null ? null : normalizeNullable(driverCard.nation());
Integer cardNationNumericCode = driverCard == null ? null : driverCard.nationNumericCode(); Integer cardNationNumericCode = driverCard == null ? null : driverCard.nationNumericCode();
String cardNumber = driverCard == null ? null : normalizeDriverCardNumber(cardNation, driverCard.number()); String cardNumber = driverCard == null ? null : normalizeDriverCardNumber(driverCard.number());
UUID driverId = findBySourceDriverEntityId(normalizedTenantKey, eventSourceId, sourceDriverEntityId); UUID driverId = findBySourceDriverEntityId(normalizedTenantKey, eventSourceId, sourceDriverEntityId);
UUID driverCardId = resolveOrCreateDriverCardId(cardNation, cardNationNumericCode, cardNumber, driverId); UUID driverCardId = resolveOrCreateDriverCardId(cardNation, cardNationNumericCode, cardNumber, driverId);
@ -782,20 +780,8 @@ public class DriverIdentityRepository {
return legacyColumns != null && legacyColumns == 3; return legacyColumns != null && legacyColumns == 3;
} }
private String normalizeDriverCardNumber(String cardNation, String cardNumber) { private String normalizeDriverCardNumber(String cardNumber) {
String normalized = normalizeNullable(cardNumber); return DriverCardNumberNormalizer.canonical(cardNumber);
if (normalized == null) {
return null;
}
if (isSyntheticYellowFoxCardNation(cardNation)) {
return normalized.length() <= 14 ? normalized : normalized.substring(0, 14);
}
return normalized;
}
private boolean isSyntheticYellowFoxCardNation(String cardNation) {
return YELLOWFOX_SYNTHETIC_REFERENCE_NATION.equalsIgnoreCase(cardNation)
|| UNKNOWN_CARD_NATION.equalsIgnoreCase(cardNation);
} }
private UUID createDriverCard( private UUID createDriverCard(

View File

@ -1,5 +1,6 @@
package at.procon.eventhub.processing.model; package at.procon.eventhub.processing.model;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@ -25,7 +26,7 @@ public record UnifiedDriverEventsRequest(
tenantKey = normalize(tenantKey); tenantKey = normalize(tenantKey);
driverSourceEntityId = normalize(driverSourceEntityId); driverSourceEntityId = normalize(driverSourceEntityId);
driverCardNation = normalizeUpper(driverCardNation); driverCardNation = normalizeUpper(driverCardNation);
driverCardNumber = normalize(driverCardNumber); driverCardNumber = normalizeDriverCardNumber(driverCardNumber);
vehicleSourceEntityId = normalize(vehicleSourceEntityId); vehicleSourceEntityId = normalize(vehicleSourceEntityId);
vin = normalizeUpper(vin); vin = normalizeUpper(vin);
registrationNation = normalizeUpper(registrationNation); registrationNation = normalizeUpper(registrationNation);
@ -171,4 +172,8 @@ public record UnifiedDriverEventsRequest(
private static String normalizeUpper(String value) { private static String normalizeUpper(String value) {
return value == null || value.isBlank() ? null : value.trim().toUpperCase(); return value == null || value.isBlank() ? null : value.trim().toUpperCase();
} }
private static String normalizeDriverCardNumber(String value) {
return DriverCardNumberNormalizer.canonical(value);
}
} }

View File

@ -1,5 +1,6 @@
package at.procon.eventhub.processing.model; package at.procon.eventhub.processing.model;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -23,7 +24,7 @@ public record UnifiedRuntimeProcessingRequest(
tenantKey = normalize(tenantKey); tenantKey = normalize(tenantKey);
driverSourceEntityId = normalize(driverSourceEntityId); driverSourceEntityId = normalize(driverSourceEntityId);
driverCardNation = normalizeUpper(driverCardNation); driverCardNation = normalizeUpper(driverCardNation);
driverCardNumber = normalize(driverCardNumber); driverCardNumber = normalizeDriverCardNumber(driverCardNumber);
boolean includesFileSession = sourceFamilies != null && sourceFamilies.contains(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION); boolean includesFileSession = sourceFamilies != null && sourceFamilies.contains(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION);
boolean includesExternalDb = sourceFamilies != null && sourceFamilies.stream() boolean includesExternalDb = sourceFamilies != null && sourceFamilies.stream()
.anyMatch(family -> family == UnifiedEventSourceFamily.TACHOGRAPH_DB || family == UnifiedEventSourceFamily.YELLOWFOX_DB); .anyMatch(family -> family == UnifiedEventSourceFamily.TACHOGRAPH_DB || family == UnifiedEventSourceFamily.YELLOWFOX_DB);
@ -196,4 +197,8 @@ public record UnifiedRuntimeProcessingRequest(
private static String normalizeUpper(String value) { private static String normalizeUpper(String value) {
return value == null || value.isBlank() ? null : value.trim().toUpperCase(); return value == null || value.isBlank() ? null : value.trim().toUpperCase();
} }
private static String normalizeDriverCardNumber(String value) {
return DriverCardNumberNormalizer.canonical(value);
}
} }

View File

@ -136,6 +136,7 @@ public class UnifiedEventTimelineReconstructor {
BigDecimal longitude = event.position() == null ? null : event.position().longitude(); BigDecimal longitude = event.position() == null ? null : event.position().longitude();
result.add(new ExtractedSupportEvent( result.add(new ExtractedSupportEvent(
eventId, eventId,
text(raw, "driverKey"),
event.occurredAt(), event.occurredAt(),
event.eventDomain().name(), event.eventDomain().name(),
text(raw, "supportEventType") == null ? event.eventType().name() : text(raw, "supportEventType"), text(raw, "supportEventType") == null ? event.eventType().name() : text(raw, "supportEventType"),

View File

@ -0,0 +1,30 @@
package at.procon.eventhub.reference;
public final class DriverCardNumberNormalizer {
public static final int CANONICAL_LENGTH = 14;
private DriverCardNumberNormalizer() {
}
public static String canonical(String rawCardNumber) {
String normalized = normalize(rawCardNumber);
if (normalized == null) {
return null;
}
return normalized.length() <= CANONICAL_LENGTH
? normalized
: normalized.substring(0, CANONICAL_LENGTH);
}
public static String full(String rawCardNumber) {
return normalize(rawCardNumber);
}
private static String normalize(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value.trim().toUpperCase();
}
}

View File

@ -6,6 +6,7 @@ public record ExtractedDriverCard(
String sourceDriverCardId, String sourceDriverCardId,
String cardNation, String cardNation,
String cardNumber, String cardNumber,
String fullCardNumber,
String issuingAuthorityName, String issuingAuthorityName,
OffsetDateTime issueDate, OffsetDateTime issueDate,
OffsetDateTime validityBegin, OffsetDateTime validityBegin,

View File

@ -5,6 +5,7 @@ import java.time.OffsetDateTime;
public record ExtractedSupportEvent( public record ExtractedSupportEvent(
String eventId, String eventId,
String driverKey,
OffsetDateTime occurredAt, OffsetDateTime occurredAt,
String eventDomain, String eventDomain,
String eventType, String eventType,

View File

@ -60,6 +60,7 @@ public class DriverCardXmlExtractionService {
driverKeyFactory.createSourceDriverCardId(driverKey), driverKeyFactory.createSourceDriverCardId(driverKey),
driverCard.cardNation(), driverCard.cardNation(),
driverCard.cardNumber(), driverCard.cardNumber(),
driverCard.fullCardNumber(),
driverCard.issuingAuthorityName(), driverCard.issuingAuthorityName(),
driverCard.issueDate(), driverCard.issueDate(),
driverCard.validityBegin(), driverCard.validityBegin(),
@ -73,7 +74,7 @@ public class DriverCardXmlExtractionService {
extractVehicleUsageIntervals(document, registrationsByKey, vehiclesByKey, warnings); extractVehicleUsageIntervals(document, registrationsByKey, vehiclesByKey, warnings);
List<ExtractedCardActivityInterval> activityIntervals = List<ExtractedCardActivityInterval> activityIntervals =
assignVehicleCoverage(extractActivityIntervals(document, warnings), vehicleUsageIntervals); assignVehicleCoverage(extractActivityIntervals(document, warnings), vehicleUsageIntervals);
List<ExtractedSupportEvent> supportEvents = extractSupportEvents(document, vehicleUsageIntervals, warnings); List<ExtractedSupportEvent> supportEvents = extractSupportEvents(document, driverKey, vehicleUsageIntervals, warnings);
DriverExtractionSession driverSession = new DriverExtractionSession( DriverExtractionSession driverSession = new DriverExtractionSession(
driverKey, driverKey,
@ -114,12 +115,14 @@ public class DriverCardXmlExtractionService {
} }
Element cardIdentification = child(identification, "cardIdentification"); Element cardIdentification = child(identification, "cardIdentification");
String cardNation = childText(cardIdentification, "cardIssuingMemberState"); String cardNation = childText(cardIdentification, "cardIssuingMemberState");
String cardNumber = joinCardNumber(identification); String fullCardNumber = joinCardNumber(identification);
String cardNumber = driverKeyFactory.canonicalCardNumber(fullCardNumber);
String authority = childText(child(cardIdentification, "cardIssuingAuthorityName"), "name"); String authority = childText(child(cardIdentification, "cardIssuingAuthorityName"), "name");
return new ExtractedDriverCard( return new ExtractedDriverCard(
null, null,
cardNation, cardNation,
cardNumber, cardNumber,
fullCardNumber,
authority, authority,
offsetDateTime(childText(cardIdentification, "cardIssueDate")), offsetDateTime(childText(cardIdentification, "cardIssueDate")),
offsetDateTime(childText(cardIdentification, "cardValidityBegin")), offsetDateTime(childText(cardIdentification, "cardValidityBegin")),
@ -307,17 +310,18 @@ public class DriverCardXmlExtractionService {
private List<ExtractedSupportEvent> extractSupportEvents( private List<ExtractedSupportEvent> extractSupportEvents(
Document document, Document document,
String driverKey,
List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals, List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals,
List<ExtractionWarning> warnings List<ExtractionWarning> warnings
) { ) {
VehicleUsageLookup vehicleUsageLookup = new VehicleUsageLookup(vehicleUsageIntervals); VehicleUsageLookup vehicleUsageLookup = new VehicleUsageLookup(vehicleUsageIntervals);
List<ExtractedSupportEvent> supportEvents = new ArrayList<>(); List<ExtractedSupportEvent> supportEvents = new ArrayList<>();
Element root = document.getDocumentElement(); Element root = document.getDocumentElement();
extractCardPlaceSupportEvents(root, vehicleUsageLookup, supportEvents, warnings); extractCardPlaceSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
extractCardGnssSupportEvents(root, vehicleUsageLookup, supportEvents, warnings); extractCardGnssSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
extractCardSpecificConditionSupportEvents(root, vehicleUsageLookup, supportEvents, warnings); extractCardSpecificConditionSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
extractCardBorderCrossingSupportEvents(root, vehicleUsageLookup, supportEvents, warnings); extractCardBorderCrossingSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
extractCardLoadUnloadSupportEvents(root, vehicleUsageLookup, supportEvents, warnings); extractCardLoadUnloadSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
supportEvents.sort(Comparator.comparing(ExtractedSupportEvent::occurredAt) supportEvents.sort(Comparator.comparing(ExtractedSupportEvent::occurredAt)
.thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo)) .thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo))
.thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo))); .thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo)));
@ -326,6 +330,7 @@ public class DriverCardXmlExtractionService {
private void extractCardPlaceSupportEvents( private void extractCardPlaceSupportEvents(
Element root, Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup, VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents, List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings List<ExtractionWarning> warnings
@ -344,6 +349,10 @@ public class DriverCardXmlExtractionService {
} }
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt); ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
if (!hasKnownRegistration(usage)) {
warnings.add(new ExtractionWarning("CARD_PLACE_UNKNOWN_VRN", "Driver-card place record has unknown vehicle registration and was ignored.", path));
continue;
}
Element gnss = child(record, "entryGnssPlaceRecord"); Element gnss = child(record, "entryGnssPlaceRecord");
Element geoCoordinates = child(gnss, "geoCoordinates"); Element geoCoordinates = child(gnss, "geoCoordinates");
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true); BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
@ -352,6 +361,7 @@ public class DriverCardXmlExtractionService {
String entryType = childText(record, "entryTypeDailyWorkPeriod"); String entryType = childText(record, "entryTypeDailyWorkPeriod");
supportEvents.add(new ExtractedSupportEvent( supportEvents.add(new ExtractedSupportEvent(
"CARDPLACE-" + (i + 1), "CARDPLACE-" + (i + 1),
driverKey,
occurredAt, occurredAt,
"PLACE", "PLACE",
mapPlaceEntryType(entryType), mapPlaceEntryType(entryType),
@ -379,6 +389,7 @@ public class DriverCardXmlExtractionService {
private void extractCardGnssSupportEvents( private void extractCardGnssSupportEvents(
Element root, Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup, VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents, List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings List<ExtractionWarning> warnings
@ -397,6 +408,11 @@ public class DriverCardXmlExtractionService {
} }
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt); ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
if (!hasKnownRegistration(usage)) {
warnings.add(new ExtractionWarning("CARD_GNSS_MISSING_VRN", "Driver-card GNSS record is missing vehicle registration.", path));
continue;
}
Element gnss = child(record, "gnssPlaceRecord"); Element gnss = child(record, "gnssPlaceRecord");
Element geoCoordinates = child(gnss, "geoCoordinates"); Element geoCoordinates = child(gnss, "geoCoordinates");
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true); BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
@ -404,6 +420,7 @@ public class DriverCardXmlExtractionService {
String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus")); String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
supportEvents.add(new ExtractedSupportEvent( supportEvents.add(new ExtractedSupportEvent(
"CARDGNSS-" + (i + 1), "CARDGNSS-" + (i + 1),
driverKey,
occurredAt, occurredAt,
"POSITION", "POSITION",
"POSITION_RECORDED", "POSITION_RECORDED",
@ -431,6 +448,7 @@ public class DriverCardXmlExtractionService {
private void extractCardSpecificConditionSupportEvents( private void extractCardSpecificConditionSupportEvents(
Element root, Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup, VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents, List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings List<ExtractionWarning> warnings
@ -449,10 +467,15 @@ public class DriverCardXmlExtractionService {
} }
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt); ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
if (!hasKnownRegistration(usage)) {
warnings.add(new ExtractionWarning("CARD_SPECIFIC_CONDITION_UNKNOWN_VRN", "Driver-card specific-condition record has unknown vehicle registration and was ignored.", path));
continue;
}
String conditionCode = normalizeToken(childText(record, "specificConditionType")); String conditionCode = normalizeToken(childText(record, "specificConditionType"));
String[] specificCondition = mapSpecificCondition(conditionCode); String[] specificCondition = mapSpecificCondition(conditionCode);
supportEvents.add(new ExtractedSupportEvent( supportEvents.add(new ExtractedSupportEvent(
"CARDSC-" + (i + 1), "CARDSC-" + (i + 1),
driverKey,
occurredAt, occurredAt,
"SPECIFIC_CONDITION", "SPECIFIC_CONDITION",
specificCondition[0], specificCondition[0],
@ -480,6 +503,7 @@ public class DriverCardXmlExtractionService {
private void extractCardBorderCrossingSupportEvents( private void extractCardBorderCrossingSupportEvents(
Element root, Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup, VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents, List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings List<ExtractionWarning> warnings
@ -499,12 +523,17 @@ public class DriverCardXmlExtractionService {
} }
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt); ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
if (!hasKnownRegistration(usage)) {
warnings.add(new ExtractionWarning("CARD_BORDER_CROSSING_UNKNOWN_VRN", "Driver-card border-crossing record has unknown vehicle registration and was ignored.", path));
continue;
}
Element geoCoordinates = child(gnss, "geoCoordinates"); Element geoCoordinates = child(gnss, "geoCoordinates");
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true); BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false); BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus")); String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
supportEvents.add(new ExtractedSupportEvent( supportEvents.add(new ExtractedSupportEvent(
"CARDBORDER-" + (i + 1), "CARDBORDER-" + (i + 1),
driverKey,
occurredAt, occurredAt,
"BORDER_CROSSING", "BORDER_CROSSING",
"BORDER_OUTBOUND", "BORDER_OUTBOUND",
@ -532,6 +561,7 @@ public class DriverCardXmlExtractionService {
private void extractCardLoadUnloadSupportEvents( private void extractCardLoadUnloadSupportEvents(
Element root, Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup, VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents, List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings List<ExtractionWarning> warnings
@ -550,6 +580,10 @@ public class DriverCardXmlExtractionService {
} }
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt); ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
if (!hasKnownRegistration(usage)) {
warnings.add(new ExtractionWarning("CARD_LOAD_UNLOAD_UNKNOWN_VRN", "Driver-card load/unload record has unknown vehicle registration and was ignored.", path));
continue;
}
Element gnss = child(record, "gnssPlaceAuthRecord"); Element gnss = child(record, "gnssPlaceAuthRecord");
Element geoCoordinates = child(gnss, "geoCoordinates"); Element geoCoordinates = child(gnss, "geoCoordinates");
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true); BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
@ -558,6 +592,7 @@ public class DriverCardXmlExtractionService {
String operation = mapOperation(childText(record, "operationType")); String operation = mapOperation(childText(record, "operationType"));
supportEvents.add(new ExtractedSupportEvent( supportEvents.add(new ExtractedSupportEvent(
"CARDLOAD-" + (i + 1), "CARDLOAD-" + (i + 1),
driverKey,
occurredAt, occurredAt,
"LOAD_UNLOAD", "LOAD_UNLOAD",
operation, operation,
@ -640,6 +675,17 @@ public class DriverCardXmlExtractionService {
return !usage.from().isAfter(timestamp) && timestamp.isBefore(usageEndExclusive(usage, null)); return !usage.from().isAfter(timestamp) && timestamp.isBefore(usageEndExclusive(usage, null));
} }
private boolean hasKnownRegistration(ExtractedCardVehicleUsageInterval usage) {
return usage != null && hasKnownRegistrationKey(usage.registrationKey());
}
private boolean hasKnownRegistrationKey(String registrationKey) {
return registrationKey != null
&& !registrationKey.isBlank()
&& !registrationKey.startsWith("UNKNOWN:")
&& !registrationKey.endsWith(":UNKNOWN");
}
private OffsetDateTime usageEndExclusive(ExtractedCardVehicleUsageInterval usage, OffsetDateTime fallbackExclusiveEnd) { private OffsetDateTime usageEndExclusive(ExtractedCardVehicleUsageInterval usage, OffsetDateTime fallbackExclusiveEnd) {
if (usage.to() == null) { if (usage.to() == null) {
return fallbackExclusiveEnd == null ? OffsetDateTime.MAX : fallbackExclusiveEnd; return fallbackExclusiveEnd == null ? OffsetDateTime.MAX : fallbackExclusiveEnd;

View File

@ -1,5 +1,6 @@
package at.procon.eventhub.tachographfilesession.service; package at.procon.eventhub.tachographfilesession.service;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@ -7,10 +8,18 @@ public class DriverKeyFactory {
public String createDriverKey(String cardNation, String cardNumber) { public String createDriverKey(String cardNation, String cardNumber) {
String normalizedNation = normalize(cardNation, "UNKNOWN"); String normalizedNation = normalize(cardNation, "UNKNOWN");
String normalizedCardNumber = normalize(cardNumber, "UNKNOWN"); String normalizedCardNumber = normalize(canonicalCardNumber(cardNumber), "UNKNOWN");
return normalizedNation + ":" + normalizedCardNumber; return normalizedNation + ":" + normalizedCardNumber;
} }
public String canonicalCardNumber(String cardNumber) {
return DriverCardNumberNormalizer.canonical(cardNumber);
}
public String fullCardNumber(String cardNumber) {
return DriverCardNumberNormalizer.full(cardNumber);
}
public String createSourceDriverId(String driverKey) { public String createSourceDriverId(String driverKey) {
return "DRV:" + driverKey; return "DRV:" + driverKey;
} }
@ -23,6 +32,6 @@ public class DriverKeyFactory {
if (value == null || value.isBlank()) { if (value == null || value.isBlank()) {
return fallback; return fallback;
} }
return value.trim(); return value.trim().toUpperCase();
} }
} }

View File

@ -111,9 +111,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
eventSource, eventSource,
sourcePackageRef sourcePackageRef
); );
List<EventHubEventDto> supportEvents = List.of(); List<EventHubEventDto> supportEvents = buildSupportEvents(
/*
buildSupportEvents(
session, session,
timeline.supportEvents(), timeline.supportEvents(),
driverRef, driverRef,
@ -122,7 +120,6 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
eventSource, eventSource,
sourcePackageRef sourcePackageRef
); );
*/
return new TachographTimelineEventBundle(activityEvents, vehicleUsageEvents, supportEvents); return new TachographTimelineEventBundle(activityEvents, vehicleUsageEvents, supportEvents);
} }
@ -309,6 +306,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
Map<String, Object> raw = new LinkedHashMap<>(); Map<String, Object> raw = new LinkedHashMap<>();
raw.put("sourceRowId", supportEvent.eventId()); raw.put("sourceRowId", supportEvent.eventId());
raw.put("supportEventId", supportEvent.eventId()); raw.put("supportEventId", supportEvent.eventId());
raw.put("driverKey", supportEvent.driverKey());
raw.put("supportEventType", supportEvent.eventType()); raw.put("supportEventType", supportEvent.eventType());
raw.put("slot", supportEvent.slot()); raw.put("slot", supportEvent.slot());
raw.put("registrationKey", supportEvent.registrationKey()); raw.put("registrationKey", supportEvent.registrationKey());

View File

@ -64,7 +64,8 @@ public class VehicleUnitXmlExtractionService {
continue; continue;
} }
String cardNation = text(record, "fullCardNumber/cardIssuingMemberState"); String cardNation = text(record, "fullCardNumber/cardIssuingMemberState");
String cardNumber = joinCardNumber(record, "fullCardNumber/cardNumber"); String fullCardNumber = joinCardNumber(record, "fullCardNumber/cardNumber");
String cardNumber = driverKeyFactory.canonicalCardNumber(fullCardNumber);
if (cardNumber == null) { if (cardNumber == null) {
sessionWarnings.add(new ExtractionWarning( sessionWarnings.add(new ExtractionWarning(
"MISSING_VU_DRIVER_CARD", "MISSING_VU_DRIVER_CARD",
@ -86,6 +87,7 @@ public class VehicleUnitXmlExtractionService {
driverKeyFactory.createSourceDriverCardId(driverKey), driverKeyFactory.createSourceDriverCardId(driverKey),
cardNation, cardNation,
cardNumber, cardNumber,
fullCardNumber,
null, null,
null, null,
null, null,
@ -366,6 +368,14 @@ public class VehicleUnitXmlExtractionService {
Map<String, DriverExtractionBuilder> driversByKey, Map<String, DriverExtractionBuilder> driversByKey,
List<ExtractionWarning> warnings List<ExtractionWarning> warnings
) { ) {
if (!hasKnownRegistration(vehicleContext.registration())) {
warnings.add(new ExtractionWarning(
"VU_SUPPORT_EVENTS_UNKNOWN_VRN",
"Vehicle-unit support events were ignored because vehicle registration is unknown.",
"/VehicleUnit/Activities"
));
return;
}
extractVuPlaceSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings); extractVuPlaceSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
extractVuGnssSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings); extractVuGnssSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
extractVuSpecificConditionSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings); extractVuSpecificConditionSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
@ -415,6 +425,7 @@ public class VehicleUnitXmlExtractionService {
assignment, assignment,
new ExtractedSupportEvent( new ExtractedSupportEvent(
"VUPLACE-" + (i + 1) + "-" + assignment.driverKey(), "VUPLACE-" + (i + 1) + "-" + assignment.driverKey(),
assignment.driverKey(),
occurredAt, occurredAt,
"PLACE", "PLACE",
mapPlaceEntryType(entryType), mapPlaceEntryType(entryType),
@ -487,6 +498,7 @@ public class VehicleUnitXmlExtractionService {
assignment, assignment,
new ExtractedSupportEvent( new ExtractedSupportEvent(
"VUGNSS-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(), "VUGNSS-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
assignment.driverKey(),
occurredAt, occurredAt,
"POSITION", "POSITION",
"POSITION_RECORDED", "POSITION_RECORDED",
@ -553,6 +565,7 @@ public class VehicleUnitXmlExtractionService {
assignment, assignment,
new ExtractedSupportEvent( new ExtractedSupportEvent(
"VUSC-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(), "VUSC-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
assignment.driverKey(),
occurredAt, occurredAt,
"SPECIFIC_CONDITION", "SPECIFIC_CONDITION",
specificCondition[0], specificCondition[0],
@ -625,6 +638,7 @@ public class VehicleUnitXmlExtractionService {
assignment, assignment,
new ExtractedSupportEvent( new ExtractedSupportEvent(
"VUBORDER-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(), "VUBORDER-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
assignment.driverKey(),
occurredAt, occurredAt,
"BORDER_CROSSING", "BORDER_CROSSING",
"BORDER_OUTBOUND", "BORDER_OUTBOUND",
@ -698,6 +712,7 @@ public class VehicleUnitXmlExtractionService {
assignment, assignment,
new ExtractedSupportEvent( new ExtractedSupportEvent(
"VULOAD-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(), "VULOAD-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
assignment.driverKey(),
occurredAt, occurredAt,
"LOAD_UNLOAD", "LOAD_UNLOAD",
operation, operation,
@ -763,6 +778,7 @@ public class VehicleUnitXmlExtractionService {
assignment, assignment,
new ExtractedSupportEvent( new ExtractedSupportEvent(
"VUSPEED-" + (i + 1) + "-" + assignment.driverKey() + "-BEGIN", "VUSPEED-" + (i + 1) + "-" + assignment.driverKey() + "-BEGIN",
assignment.driverKey(),
beginAt, beginAt,
"SPEEDING", "SPEEDING",
"SPEEDING", "SPEEDING",
@ -793,6 +809,7 @@ public class VehicleUnitXmlExtractionService {
assignment, assignment,
new ExtractedSupportEvent( new ExtractedSupportEvent(
"VUSPEED-" + (i + 1) + "-" + assignment.driverKey() + "-END", "VUSPEED-" + (i + 1) + "-" + assignment.driverKey() + "-END",
assignment.driverKey(),
endAt, endAt,
"SPEEDING", "SPEEDING",
"SPEEDING", "SPEEDING",
@ -899,6 +916,17 @@ public class VehicleUnitXmlExtractionService {
builder.supportEvents.add(supportEvent); builder.supportEvents.add(supportEvent);
} }
private boolean hasKnownRegistration(ExtractedVehicleRegistration registration) {
return registration != null && hasKnownRegistrationKey(registration.registrationKey());
}
private boolean hasKnownRegistrationKey(String registrationKey) {
return registrationKey != null
&& !registrationKey.isBlank()
&& !registrationKey.startsWith("UNKNOWN:")
&& !registrationKey.endsWith(":UNKNOWN");
}
private List<DriverAssignment> resolveDriverAssignments( private List<DriverAssignment> resolveDriverAssignments(
OffsetDateTime occurredAt, OffsetDateTime occurredAt,
String explicitDriverKey, String explicitDriverKey,
@ -925,7 +953,7 @@ public class VehicleUnitXmlExtractionService {
private String driverKeyFromCardNode(Element node, String basePath) { private String driverKeyFromCardNode(Element node, String basePath) {
String cardNation = text(node, basePath + "/cardIssuingMemberState"); String cardNation = text(node, basePath + "/cardIssuingMemberState");
String cardNumber = joinCardNumber(node, basePath + "/cardNumber"); String cardNumber = driverKeyFactory.canonicalCardNumber(joinCardNumber(node, basePath + "/cardNumber"));
return cardNumber == null ? null : driverKeyFactory.createDriverKey(cardNation, cardNumber); return cardNumber == null ? null : driverKeyFactory.createDriverKey(cardNation, cardNumber);
} }
@ -1109,6 +1137,7 @@ public class VehicleUnitXmlExtractionService {
String sourceDriverCardId, String sourceDriverCardId,
String cardNation, String cardNation,
String cardNumber, String cardNumber,
String fullCardNumber,
String issuingAuthorityName, String issuingAuthorityName,
OffsetDateTime issueDate, OffsetDateTime issueDate,
OffsetDateTime validityBegin, OffsetDateTime validityBegin,
@ -1119,6 +1148,7 @@ public class VehicleUnitXmlExtractionService {
sourceDriverCardId, sourceDriverCardId,
cardNation, cardNation,
cardNumber, cardNumber,
fullCardNumber,
issuingAuthorityName, issuingAuthorityName,
issueDate, issueDate,
validityBegin, validityBegin,

View File

@ -4,6 +4,7 @@ import at.procon.eventhub.dto.DriverCardRefDto;
import at.procon.eventhub.dto.DriverRefDto; import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.VehicleRefDto; import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.dto.VehicleRegistrationRefDto; import at.procon.eventhub.dto.VehicleRegistrationRefDto;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import at.procon.eventhub.yellowfox.dto.YellowFoxD8BookingDto; import at.procon.eventhub.yellowfox.dto.YellowFoxD8BookingDto;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -149,10 +150,6 @@ public class YellowFoxD8BookingRowMapper {
} }
private String normalizeBookingDriverCardNumber(String value) { private String normalizeBookingDriverCardNumber(String value) {
if (value == null || value.isBlank()) { return DriverCardNumberNormalizer.canonical(value);
return null;
}
String trimmed = value.trim();
return trimmed.length() <= 14 ? trimmed : trimmed.substring(0, 14);
} }
} }

View File

@ -32,7 +32,7 @@ class UnifiedDriverEventsRequestTest {
" default ", " default ",
" DRIVER:42 ", " DRIVER:42 ",
"at", "at",
" 123 ", " 1234567890123457 ",
OffsetDateTime.parse("2026-05-01T00:00:00Z"), OffsetDateTime.parse("2026-05-01T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z") OffsetDateTime.parse("2026-05-02T00:00:00Z")
); );
@ -41,7 +41,7 @@ class UnifiedDriverEventsRequestTest {
assertThat(request.tenantKey()).isEqualTo("default"); assertThat(request.tenantKey()).isEqualTo("default");
assertThat(request.driverSourceEntityId()).isEqualTo("DRIVER:42"); assertThat(request.driverSourceEntityId()).isEqualTo("DRIVER:42");
assertThat(request.driverCardNation()).isEqualTo("AT"); assertThat(request.driverCardNation()).isEqualTo("AT");
assertThat(request.driverCardNumber()).isEqualTo("123"); assertThat(request.driverCardNumber()).isEqualTo("12345678901234");
assertThat(request.hasDriverSelector()).isTrue(); assertThat(request.hasDriverSelector()).isTrue();
assertThat(request.hasVehicleSelector()).isFalse(); assertThat(request.hasVehicleSelector()).isFalse();
} }

View File

@ -17,7 +17,7 @@ class UnifiedRuntimeProcessingRequestTest {
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_DB, UnifiedEventSourceFamily.YELLOWFOX_DB), Set.of(UnifiedEventSourceFamily.TACHOGRAPH_DB, UnifiedEventSourceFamily.YELLOWFOX_DB),
"DRIVER:42", "DRIVER:42",
"AT", "AT",
"123", "1234567890123457",
OffsetDateTime.parse("2026-05-01T00:00:00Z"), OffsetDateTime.parse("2026-05-01T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z") OffsetDateTime.parse("2026-05-02T00:00:00Z")
); );
@ -29,7 +29,7 @@ class UnifiedRuntimeProcessingRequestTest {
); );
assertThat(request.driverSourceEntityId()).isEqualTo("DRIVER:42"); assertThat(request.driverSourceEntityId()).isEqualTo("DRIVER:42");
assertThat(request.driverCardNation()).isEqualTo("AT"); assertThat(request.driverCardNation()).isEqualTo("AT");
assertThat(request.driverCardNumber()).isEqualTo("123"); assertThat(request.driverCardNumber()).isEqualTo("12345678901234");
assertThat(request.eventBackend()).isEqualTo(UnifiedRuntimeEventBackend.SOURCE_DB); assertThat(request.eventBackend()).isEqualTo(UnifiedRuntimeEventBackend.SOURCE_DB);
assertThat(request.expandVehicleEvents()).isTrue(); assertThat(request.expandVehicleEvents()).isTrue();
assertThat(request.vehicleOccurredFrom()).isEqualTo(OffsetDateTime.parse("2026-05-01T00:00:00Z")); assertThat(request.vehicleOccurredFrom()).isEqualTo(OffsetDateTime.parse("2026-05-01T00:00:00Z"));

View File

@ -72,7 +72,7 @@ class TachographFileSessionRuntimeEventLoaderTest {
return new DriverExtractionSession( return new DriverExtractionSession(
"12:123", "12:123",
new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null),
new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), new ExtractedDriverCard("CARD:12:123", "12", "123", "123", null, null, null, null),
List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")),
List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval( List.of(new ExtractedCardVehicleUsageInterval(
@ -99,6 +99,7 @@ class TachographFileSessionRuntimeEventLoaderTest {
)), )),
List.of(new ExtractedSupportEvent( List.of(new ExtractedSupportEvent(
"SUP-1", "SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"), OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION", "POSITION",
"POSITION_RECORDED", "POSITION_RECORDED",

View File

@ -88,7 +88,7 @@ class UnifiedDriverEventSourceServiceTest {
return new DriverExtractionSession( return new DriverExtractionSession(
"12:123", "12:123",
new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null),
new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), new ExtractedDriverCard("CARD:12:123", "12", "123", "123", null, null, null, null),
List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")),
List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval( List.of(new ExtractedCardVehicleUsageInterval(
@ -115,6 +115,7 @@ class UnifiedDriverEventSourceServiceTest {
)), )),
List.of(new ExtractedSupportEvent( List.of(new ExtractedSupportEvent(
"SUP-1", "SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"), OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION", "POSITION",
"POSITION_RECORDED", "POSITION_RECORDED",

View File

@ -73,7 +73,7 @@ class UnifiedDriverTimelineServiceTest {
return new DriverExtractionSession( return new DriverExtractionSession(
"12:123", "12:123",
new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null),
new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), new ExtractedDriverCard("CARD:12:123", "12", "123", "123", null, null, null, null),
List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")),
List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval( List.of(new ExtractedCardVehicleUsageInterval(
@ -100,6 +100,7 @@ class UnifiedDriverTimelineServiceTest {
)), )),
List.of(new ExtractedSupportEvent( List.of(new ExtractedSupportEvent(
"SUP-1", "SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"), OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION", "POSITION",
"POSITION_RECORDED", "POSITION_RECORDED",

View File

@ -93,7 +93,7 @@ class UnifiedVehicleEventSourceServiceTest {
return new DriverExtractionSession( return new DriverExtractionSession(
"12:123", "12:123",
new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null),
new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), new ExtractedDriverCard("CARD:12:123", "12", "123", "123", null, null, null, null),
List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")),
List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval( List.of(new ExtractedCardVehicleUsageInterval(
@ -120,6 +120,7 @@ class UnifiedVehicleEventSourceServiceTest {
)), )),
List.of(new ExtractedSupportEvent( List.of(new ExtractedSupportEvent(
"SUP-1", "SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"), OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION", "POSITION",
"POSITION_RECORDED", "POSITION_RECORDED",

View File

@ -110,5 +110,73 @@ class DriverCardXmlExtractionServiceTest {
assertThat(driver.cardVehicleUsageIntervals().get(1).to()).isNull(); assertThat(driver.cardVehicleUsageIntervals().get(1).to()).isNull();
assertThat(driver.cardActivityIntervals()).hasSize(3); assertThat(driver.cardActivityIntervals()).hasSize(3);
assertThat(driver.cardActivityIntervals().get(2).registrationKey()).isEqualTo("12:W-54321B"); assertThat(driver.cardActivityIntervals().get(2).registrationKey()).isEqualTo("12:W-54321B");
assertThat(driver.supportEvents()).hasSize(5);
assertThat(driver.supportEvents().stream()
.filter(event -> "12:W-54321B".equals(event.registrationKey()))
.map(event -> event.eventDomain() + ":" + event.eventType()))
.containsExactly("BORDER_CROSSING:BORDER_OUTBOUND", "LOAD_UNLOAD:UNLOAD");
}
@Test
void ignoresSupportEventsWhenVehicleRegistrationIsUnknown() {
String xml = DriverCardXmlSamples.validDriverCardXml()
.replace("<vehicleRegNumber>W-12345A</vehicleRegNumber>", "<vehicleRegNumber></vehicleRegNumber>")
.replace("<vehicleRegNumber>W-54321B</vehicleRegNumber>", "<vehicleRegNumber></vehicleRegNumber>");
TachographFileSession session = service.extract(
parser.parse(xml),
new TachographFileSessionMetadata(
"default",
"legalrequirements-drivercard",
"sample",
"sample.ddd",
"abc",
10,
"42",
"def",
true,
null
),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
DriverExtractionSession driver = session.driversByKey().values().iterator().next();
assertThat(driver.cardVehicleUsageIntervals()).hasSize(2);
assertThat(driver.cardVehicleUsageIntervals()).extracting("registrationKey")
.containsOnly("12:UNKNOWN");
assertThat(driver.supportEvents()).isEmpty();
}
@Test
void usesCanonicalCardNumberForDriverIdentityAndKeepsFullCardNumberAsEvidence() {
String xml = DriverCardXmlSamples.validDriverCardXml()
.replace("<driverIdentification>123456789012</driverIdentification>", "<driverIdentification>12345678901234</driverIdentification>")
.replace("<cardReplacementIndex>0</cardReplacementIndex>", "<cardReplacementIndex>5</cardReplacementIndex>")
.replace("<cardRenewalIndex>0</cardRenewalIndex>", "<cardRenewalIndex>7</cardRenewalIndex>");
TachographFileSession session = service.extract(
parser.parse(xml),
new TachographFileSessionMetadata(
"default",
"legalrequirements-drivercard",
"sample",
"sample.ddd",
"abc",
10,
"42",
"def",
true,
null
),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
DriverExtractionSession driver = session.driversByKey().values().iterator().next();
assertThat(driver.driverKey()).isEqualTo("12:12345678901234");
assertThat(driver.driverCard().cardNumber()).isEqualTo("12345678901234");
assertThat(driver.driverCard().fullCardNumber()).isEqualTo("1234567890123457");
assertThat(driver.driverCard().sourceDriverCardId()).isEqualTo("CARD:12:12345678901234");
} }
} }

View File

@ -74,6 +74,7 @@ class DriverTimelineBuilderTest {
), ),
List.of(new ExtractedSupportEvent( List.of(new ExtractedSupportEvent(
"SUP-1", "SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:30:00Z"), OffsetDateTime.parse("2026-05-01T08:30:00Z"),
"PLACE", "PLACE",
"BEGIN_DAILY_WORK_PERIOD", "BEGIN_DAILY_WORK_PERIOD",

View File

@ -43,7 +43,7 @@ class EventBackedDriverTimelineBuilderTest {
DriverExtractionSession driver = new DriverExtractionSession( DriverExtractionSession driver = new DriverExtractionSession(
"12:123", "12:123",
new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null),
new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), new ExtractedDriverCard("CARD:12:123", "12", "123", "123", null, null, null, null),
List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")),
List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval( List.of(new ExtractedCardVehicleUsageInterval(
@ -70,6 +70,7 @@ class EventBackedDriverTimelineBuilderTest {
)), )),
List.of(new ExtractedSupportEvent( List.of(new ExtractedSupportEvent(
"SUP-1", "SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"), OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION", "POSITION",
"POSITION_RECORDED", "POSITION_RECORDED",

View File

@ -42,7 +42,7 @@ class IntervalBackedDriverTimelineEventBuilderTest {
DriverExtractionSession driver = new DriverExtractionSession( DriverExtractionSession driver = new DriverExtractionSession(
"12:123", "12:123",
new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null),
new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), new ExtractedDriverCard("CARD:12:123", "12", "123", "123", null, null, null, null),
List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")),
List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval( List.of(new ExtractedCardVehicleUsageInterval(
@ -92,7 +92,7 @@ class IntervalBackedDriverTimelineEventBuilderTest {
DriverExtractionSession driver = new DriverExtractionSession( DriverExtractionSession driver = new DriverExtractionSession(
"12:123", "12:123",
new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null),
new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), new ExtractedDriverCard("CARD:12:123", "12", "123", "123", null, null, null, null),
List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")),
List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval( List.of(new ExtractedCardVehicleUsageInterval(
@ -108,6 +108,7 @@ class IntervalBackedDriverTimelineEventBuilderTest {
List.of(), List.of(),
List.of(new ExtractedSupportEvent( List.of(new ExtractedSupportEvent(
"VUGNSS-1", "VUGNSS-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"), OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION", "POSITION",
"POSITION_RECORDED", "POSITION_RECORDED",

View File

@ -132,6 +132,66 @@ class VehicleUnitXmlExtractionServiceTest {
assertThat(secondDriver.cardVehicleUsageIntervals().get(0).to()).isNull(); assertThat(secondDriver.cardVehicleUsageIntervals().get(0).to()).isNull();
} }
@Test
void ignoresSupportEventsWhenVehicleRegistrationIsUnknown() throws Exception {
String xml = VehicleUnitXmlSamples.vehicleUnitXml()
.replace("<vehicleRegNumber>W-1000V</vehicleRegNumber>", "<vehicleRegNumber></vehicleRegNumber>");
TachographFileSession session = service.extract(
new TachographXmlParser.ParsedTachographXml(document(xml), "VehicleUnit"),
new TachographFileSessionMetadata(
"default",
"legalrequirements-vehicleunit",
"sample-vu",
"sample-vu.ddd",
"abc",
10,
"42",
"def",
false,
null
),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
assertThat(session.driversByKey()).hasSize(2);
assertThat(session.driversByKey().values())
.allSatisfy(driver -> assertThat(driver.supportEvents()).isEmpty());
}
@Test
void usesCanonicalCardNumberForVehicleUnitDriverIdentityAndKeepsFullCardNumberAsEvidence() throws Exception {
String xml = VehicleUnitXmlSamples.vehicleUnitXml()
.replaceFirst("<driverIdentification>123456789012</driverIdentification>", "<driverIdentification>12345678901234</driverIdentification>")
.replaceFirst("<cardReplacementIndex>0</cardReplacementIndex>", "<cardReplacementIndex>5</cardReplacementIndex>")
.replaceFirst("<cardRenewalIndex>0</cardRenewalIndex>", "<cardRenewalIndex>7</cardRenewalIndex>");
TachographFileSession session = service.extract(
new TachographXmlParser.ParsedTachographXml(document(xml), "VehicleUnit"),
new TachographFileSessionMetadata(
"default",
"legalrequirements-vehicleunit",
"sample-vu",
"sample-vu.ddd",
"abc",
10,
"42",
"def",
false,
null
),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
DriverExtractionSession firstDriver = session.driversByKey().get("12:12345678901234");
assertThat(firstDriver).isNotNull();
assertThat(firstDriver.driverCard().cardNumber()).isEqualTo("12345678901234");
assertThat(firstDriver.driverCard().fullCardNumber()).isEqualTo("1234567890123457");
assertThat(firstDriver.driverCard().sourceDriverCardId()).isEqualTo("CARD:12:12345678901234");
}
private Document document(String xml) throws Exception { private Document document(String xml) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false); factory.setNamespaceAware(false);