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;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import at.procon.eventhub.reference.TachographNationRegistry;
/**
@ -43,6 +44,6 @@ public record DriverCardRefDto(
}
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.DriverRefDto;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDate;
@ -16,9 +17,6 @@ import org.springframework.transaction.annotation.Transactional;
@Repository
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 ObjectMapper objectMapper;
@ -41,7 +39,7 @@ public class DriverIdentityRepository {
DriverCardRefDto driverCard = driverRef.driverCard();
String cardNation = driverCard == null ? null : normalizeNullable(driverCard.nation());
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 driverCardId = resolveOrCreateDriverCardId(cardNation, cardNationNumericCode, cardNumber, driverId);
@ -782,20 +780,8 @@ public class DriverIdentityRepository {
return legacyColumns != null && legacyColumns == 3;
}
private String normalizeDriverCardNumber(String cardNation, String cardNumber) {
String normalized = normalizeNullable(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 String normalizeDriverCardNumber(String cardNumber) {
return DriverCardNumberNormalizer.canonical(cardNumber);
}
private UUID createDriverCard(

View File

@ -1,5 +1,6 @@
package at.procon.eventhub.processing.model;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import java.time.OffsetDateTime;
import java.util.Objects;
import java.util.UUID;
@ -25,7 +26,7 @@ public record UnifiedDriverEventsRequest(
tenantKey = normalize(tenantKey);
driverSourceEntityId = normalize(driverSourceEntityId);
driverCardNation = normalizeUpper(driverCardNation);
driverCardNumber = normalize(driverCardNumber);
driverCardNumber = normalizeDriverCardNumber(driverCardNumber);
vehicleSourceEntityId = normalize(vehicleSourceEntityId);
vin = normalizeUpper(vin);
registrationNation = normalizeUpper(registrationNation);
@ -171,4 +172,8 @@ public record UnifiedDriverEventsRequest(
private static String normalizeUpper(String value) {
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;
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
import java.time.OffsetDateTime;
import java.util.Set;
import java.util.UUID;
@ -23,7 +24,7 @@ public record UnifiedRuntimeProcessingRequest(
tenantKey = normalize(tenantKey);
driverSourceEntityId = normalize(driverSourceEntityId);
driverCardNation = normalizeUpper(driverCardNation);
driverCardNumber = normalize(driverCardNumber);
driverCardNumber = normalizeDriverCardNumber(driverCardNumber);
boolean includesFileSession = sourceFamilies != null && sourceFamilies.contains(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION);
boolean includesExternalDb = sourceFamilies != null && sourceFamilies.stream()
.anyMatch(family -> family == UnifiedEventSourceFamily.TACHOGRAPH_DB || family == UnifiedEventSourceFamily.YELLOWFOX_DB);
@ -196,4 +197,8 @@ public record UnifiedRuntimeProcessingRequest(
private static String normalizeUpper(String value) {
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();
result.add(new ExtractedSupportEvent(
eventId,
text(raw, "driverKey"),
event.occurredAt(),
event.eventDomain().name(),
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 cardNation,
String cardNumber,
String fullCardNumber,
String issuingAuthorityName,
OffsetDateTime issueDate,
OffsetDateTime validityBegin,

View File

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

View File

@ -60,6 +60,7 @@ public class DriverCardXmlExtractionService {
driverKeyFactory.createSourceDriverCardId(driverKey),
driverCard.cardNation(),
driverCard.cardNumber(),
driverCard.fullCardNumber(),
driverCard.issuingAuthorityName(),
driverCard.issueDate(),
driverCard.validityBegin(),
@ -73,7 +74,7 @@ public class DriverCardXmlExtractionService {
extractVehicleUsageIntervals(document, registrationsByKey, vehiclesByKey, warnings);
List<ExtractedCardActivityInterval> activityIntervals =
assignVehicleCoverage(extractActivityIntervals(document, warnings), vehicleUsageIntervals);
List<ExtractedSupportEvent> supportEvents = extractSupportEvents(document, vehicleUsageIntervals, warnings);
List<ExtractedSupportEvent> supportEvents = extractSupportEvents(document, driverKey, vehicleUsageIntervals, warnings);
DriverExtractionSession driverSession = new DriverExtractionSession(
driverKey,
@ -114,12 +115,14 @@ public class DriverCardXmlExtractionService {
}
Element cardIdentification = child(identification, "cardIdentification");
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");
return new ExtractedDriverCard(
null,
cardNation,
cardNumber,
fullCardNumber,
authority,
offsetDateTime(childText(cardIdentification, "cardIssueDate")),
offsetDateTime(childText(cardIdentification, "cardValidityBegin")),
@ -307,17 +310,18 @@ public class DriverCardXmlExtractionService {
private List<ExtractedSupportEvent> extractSupportEvents(
Document document,
String driverKey,
List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals,
List<ExtractionWarning> warnings
) {
VehicleUsageLookup vehicleUsageLookup = new VehicleUsageLookup(vehicleUsageIntervals);
List<ExtractedSupportEvent> supportEvents = new ArrayList<>();
Element root = document.getDocumentElement();
extractCardPlaceSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
extractCardGnssSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
extractCardSpecificConditionSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
extractCardBorderCrossingSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
extractCardLoadUnloadSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
extractCardPlaceSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
extractCardGnssSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
extractCardSpecificConditionSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
extractCardBorderCrossingSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
extractCardLoadUnloadSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
supportEvents.sort(Comparator.comparing(ExtractedSupportEvent::occurredAt)
.thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo))
.thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo)));
@ -326,6 +330,7 @@ public class DriverCardXmlExtractionService {
private void extractCardPlaceSupportEvents(
Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings
@ -344,6 +349,10 @@ public class DriverCardXmlExtractionService {
}
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 geoCoordinates = child(gnss, "geoCoordinates");
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
@ -352,6 +361,7 @@ public class DriverCardXmlExtractionService {
String entryType = childText(record, "entryTypeDailyWorkPeriod");
supportEvents.add(new ExtractedSupportEvent(
"CARDPLACE-" + (i + 1),
driverKey,
occurredAt,
"PLACE",
mapPlaceEntryType(entryType),
@ -379,6 +389,7 @@ public class DriverCardXmlExtractionService {
private void extractCardGnssSupportEvents(
Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings
@ -397,6 +408,11 @@ public class DriverCardXmlExtractionService {
}
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 geoCoordinates = child(gnss, "geoCoordinates");
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
@ -404,6 +420,7 @@ public class DriverCardXmlExtractionService {
String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
supportEvents.add(new ExtractedSupportEvent(
"CARDGNSS-" + (i + 1),
driverKey,
occurredAt,
"POSITION",
"POSITION_RECORDED",
@ -431,6 +448,7 @@ public class DriverCardXmlExtractionService {
private void extractCardSpecificConditionSupportEvents(
Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings
@ -449,10 +467,15 @@ public class DriverCardXmlExtractionService {
}
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[] specificCondition = mapSpecificCondition(conditionCode);
supportEvents.add(new ExtractedSupportEvent(
"CARDSC-" + (i + 1),
driverKey,
occurredAt,
"SPECIFIC_CONDITION",
specificCondition[0],
@ -480,6 +503,7 @@ public class DriverCardXmlExtractionService {
private void extractCardBorderCrossingSupportEvents(
Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings
@ -499,12 +523,17 @@ public class DriverCardXmlExtractionService {
}
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");
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
supportEvents.add(new ExtractedSupportEvent(
"CARDBORDER-" + (i + 1),
driverKey,
occurredAt,
"BORDER_CROSSING",
"BORDER_OUTBOUND",
@ -532,6 +561,7 @@ public class DriverCardXmlExtractionService {
private void extractCardLoadUnloadSupportEvents(
Element root,
String driverKey,
VehicleUsageLookup vehicleUsageLookup,
List<ExtractedSupportEvent> supportEvents,
List<ExtractionWarning> warnings
@ -550,6 +580,10 @@ public class DriverCardXmlExtractionService {
}
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 geoCoordinates = child(gnss, "geoCoordinates");
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
@ -558,6 +592,7 @@ public class DriverCardXmlExtractionService {
String operation = mapOperation(childText(record, "operationType"));
supportEvents.add(new ExtractedSupportEvent(
"CARDLOAD-" + (i + 1),
driverKey,
occurredAt,
"LOAD_UNLOAD",
operation,
@ -640,6 +675,17 @@ public class DriverCardXmlExtractionService {
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) {
if (usage.to() == null) {
return fallbackExclusiveEnd == null ? OffsetDateTime.MAX : fallbackExclusiveEnd;

View File

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

View File

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

View File

@ -64,7 +64,8 @@ public class VehicleUnitXmlExtractionService {
continue;
}
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) {
sessionWarnings.add(new ExtractionWarning(
"MISSING_VU_DRIVER_CARD",
@ -86,6 +87,7 @@ public class VehicleUnitXmlExtractionService {
driverKeyFactory.createSourceDriverCardId(driverKey),
cardNation,
cardNumber,
fullCardNumber,
null,
null,
null,
@ -366,6 +368,14 @@ public class VehicleUnitXmlExtractionService {
Map<String, DriverExtractionBuilder> driversByKey,
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);
extractVuGnssSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
extractVuSpecificConditionSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
@ -415,6 +425,7 @@ public class VehicleUnitXmlExtractionService {
assignment,
new ExtractedSupportEvent(
"VUPLACE-" + (i + 1) + "-" + assignment.driverKey(),
assignment.driverKey(),
occurredAt,
"PLACE",
mapPlaceEntryType(entryType),
@ -487,6 +498,7 @@ public class VehicleUnitXmlExtractionService {
assignment,
new ExtractedSupportEvent(
"VUGNSS-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
assignment.driverKey(),
occurredAt,
"POSITION",
"POSITION_RECORDED",
@ -553,6 +565,7 @@ public class VehicleUnitXmlExtractionService {
assignment,
new ExtractedSupportEvent(
"VUSC-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
assignment.driverKey(),
occurredAt,
"SPECIFIC_CONDITION",
specificCondition[0],
@ -625,6 +638,7 @@ public class VehicleUnitXmlExtractionService {
assignment,
new ExtractedSupportEvent(
"VUBORDER-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
assignment.driverKey(),
occurredAt,
"BORDER_CROSSING",
"BORDER_OUTBOUND",
@ -698,6 +712,7 @@ public class VehicleUnitXmlExtractionService {
assignment,
new ExtractedSupportEvent(
"VULOAD-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
assignment.driverKey(),
occurredAt,
"LOAD_UNLOAD",
operation,
@ -763,6 +778,7 @@ public class VehicleUnitXmlExtractionService {
assignment,
new ExtractedSupportEvent(
"VUSPEED-" + (i + 1) + "-" + assignment.driverKey() + "-BEGIN",
assignment.driverKey(),
beginAt,
"SPEEDING",
"SPEEDING",
@ -793,6 +809,7 @@ public class VehicleUnitXmlExtractionService {
assignment,
new ExtractedSupportEvent(
"VUSPEED-" + (i + 1) + "-" + assignment.driverKey() + "-END",
assignment.driverKey(),
endAt,
"SPEEDING",
"SPEEDING",
@ -899,6 +916,17 @@ public class VehicleUnitXmlExtractionService {
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(
OffsetDateTime occurredAt,
String explicitDriverKey,
@ -925,7 +953,7 @@ public class VehicleUnitXmlExtractionService {
private String driverKeyFromCardNode(Element node, String basePath) {
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);
}
@ -1109,6 +1137,7 @@ public class VehicleUnitXmlExtractionService {
String sourceDriverCardId,
String cardNation,
String cardNumber,
String fullCardNumber,
String issuingAuthorityName,
OffsetDateTime issueDate,
OffsetDateTime validityBegin,
@ -1119,6 +1148,7 @@ public class VehicleUnitXmlExtractionService {
sourceDriverCardId,
cardNation,
cardNumber,
fullCardNumber,
issuingAuthorityName,
issueDate,
validityBegin,

View File

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

View File

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

View File

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

View File

@ -72,7 +72,7 @@ class TachographFileSessionRuntimeEventLoaderTest {
return new DriverExtractionSession(
"12:123",
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 ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval(
@ -99,6 +99,7 @@ class TachographFileSessionRuntimeEventLoaderTest {
)),
List.of(new ExtractedSupportEvent(
"SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION",
"POSITION_RECORDED",

View File

@ -88,7 +88,7 @@ class UnifiedDriverEventSourceServiceTest {
return new DriverExtractionSession(
"12:123",
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 ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval(
@ -115,6 +115,7 @@ class UnifiedDriverEventSourceServiceTest {
)),
List.of(new ExtractedSupportEvent(
"SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION",
"POSITION_RECORDED",

View File

@ -73,7 +73,7 @@ class UnifiedDriverTimelineServiceTest {
return new DriverExtractionSession(
"12:123",
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 ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval(
@ -100,6 +100,7 @@ class UnifiedDriverTimelineServiceTest {
)),
List.of(new ExtractedSupportEvent(
"SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION",
"POSITION_RECORDED",

View File

@ -93,7 +93,7 @@ class UnifiedVehicleEventSourceServiceTest {
return new DriverExtractionSession(
"12:123",
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 ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval(
@ -120,6 +120,7 @@ class UnifiedVehicleEventSourceServiceTest {
)),
List.of(new ExtractedSupportEvent(
"SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION",
"POSITION_RECORDED",

View File

@ -110,5 +110,73 @@ class DriverCardXmlExtractionServiceTest {
assertThat(driver.cardVehicleUsageIntervals().get(1).to()).isNull();
assertThat(driver.cardActivityIntervals()).hasSize(3);
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(
"SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
"PLACE",
"BEGIN_DAILY_WORK_PERIOD",

View File

@ -43,7 +43,7 @@ class EventBackedDriverTimelineBuilderTest {
DriverExtractionSession driver = new DriverExtractionSession(
"12:123",
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 ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval(
@ -70,6 +70,7 @@ class EventBackedDriverTimelineBuilderTest {
)),
List.of(new ExtractedSupportEvent(
"SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION",
"POSITION_RECORDED",

View File

@ -42,7 +42,7 @@ class IntervalBackedDriverTimelineEventBuilderTest {
DriverExtractionSession driver = new DriverExtractionSession(
"12:123",
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 ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval(
@ -92,7 +92,7 @@ class IntervalBackedDriverTimelineEventBuilderTest {
DriverExtractionSession driver = new DriverExtractionSession(
"12:123",
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 ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
List.of(new ExtractedCardVehicleUsageInterval(
@ -108,6 +108,7 @@ class IntervalBackedDriverTimelineEventBuilderTest {
List.of(),
List.of(new ExtractedSupportEvent(
"VUGNSS-1",
"12:123",
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
"POSITION",
"POSITION_RECORDED",

View File

@ -132,6 +132,66 @@ class VehicleUnitXmlExtractionServiceTest {
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 {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);