From fc0d6db99aab3c1bb37ccc3027a946ace2048f55 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Wed, 20 May 2026 14:31:14 +0200 Subject: [PATCH] Add numeric tachograph nation resolution --- .../procon/eventhub/dto/DriverCardRefDto.java | 24 ++- .../dto/VehicleRegistrationRefDto.java | 24 ++- .../persistence/DriverIdentityRepository.java | 35 +++- .../EventHubEventReadRepository.java | 48 ++++- .../eventhub/persistence/EventRepository.java | 8 +- .../VehicleIdentityRepository.java | 48 ++++- .../TachographDbRuntimeEventLoader.java | 9 +- ...hFileSessionUnifiedVehicleEventSource.java | 18 +- .../UnifiedRuntimeEventAssemblyService.java | 6 +- .../reference/TachographNationRegistry.java | 178 ++++++++++++++++++ .../eventhub/service/EventDetailsFactory.java | 14 +- .../resources/db/eventhub_schema_create.sql | 92 +++++++++ ...add_nation_reference_and_numeric_codes.sql | 147 +++++++++++++++ src/main/resources/reference/nation.csv | 60 ++++++ .../TachographNationRegistryTest.java | 51 +++++ 15 files changed, 724 insertions(+), 38 deletions(-) create mode 100644 src/main/java/at/procon/eventhub/reference/TachographNationRegistry.java create mode 100644 src/main/resources/db/migration/V13__add_nation_reference_and_numeric_codes.sql create mode 100644 src/main/resources/reference/nation.csv create mode 100644 src/test/java/at/procon/eventhub/reference/TachographNationRegistryTest.java diff --git a/src/main/java/at/procon/eventhub/dto/DriverCardRefDto.java b/src/main/java/at/procon/eventhub/dto/DriverCardRefDto.java index 7c7b54c..7ea04a4 100644 --- a/src/main/java/at/procon/eventhub/dto/DriverCardRefDto.java +++ b/src/main/java/at/procon/eventhub/dto/DriverCardRefDto.java @@ -1,14 +1,24 @@ package at.procon.eventhub.dto; +import at.procon.eventhub.reference.TachographNationRegistry; + /** * Tachograph driver-card identifier. The card number is scoped by issuing nation. */ public record DriverCardRefDto( String nation, + Integer nationNumericCode, String number ) { + public DriverCardRefDto(String nation, String number) { + this(nation, null, number); + } + public DriverCardRefDto { - nation = normalize(nation); + TachographNationRegistry.NationResolution nationResolution = + TachographNationRegistry.resolve(nation, nationNumericCode); + nation = normalize(nationResolution.legacyNation()); + nationNumericCode = nationResolution.numericCode(); number = normalizeNullable(number); } @@ -17,11 +27,19 @@ public record DriverCardRefDto( } public String stableKey() { - return (nation == null ? "" : nation) + ":" + (number == null ? "" : number); + String nationKey = nationNumericCode == null ? nation : String.valueOf(nationNumericCode); + return (nationKey == null ? "" : nationKey) + ":" + (number == null ? "" : number); } private static String normalize(String value) { - return value == null || value.isBlank() ? null : value.trim().toUpperCase(); + if (value == null || value.isBlank()) { + return null; + } + String trimmed = value.trim(); + if (trimmed.matches("(?i)^unknown\\s+[0-9]+$")) { + return "Unknown " + trimmed.replaceAll("[^0-9]", ""); + } + return trimmed.toUpperCase(); } private static String normalizeNullable(String value) { diff --git a/src/main/java/at/procon/eventhub/dto/VehicleRegistrationRefDto.java b/src/main/java/at/procon/eventhub/dto/VehicleRegistrationRefDto.java index e34d129..318c7a5 100644 --- a/src/main/java/at/procon/eventhub/dto/VehicleRegistrationRefDto.java +++ b/src/main/java/at/procon/eventhub/dto/VehicleRegistrationRefDto.java @@ -1,15 +1,25 @@ package at.procon.eventhub.dto; +import at.procon.eventhub.reference.TachographNationRegistry; + /** * Vehicle registration number reference. VRN/plate is scoped by nation and can * be resolved historically by occurredAt later. */ public record VehicleRegistrationRefDto( String nation, + Integer nationNumericCode, String number ) { + public VehicleRegistrationRefDto(String nation, String number) { + this(nation, null, number); + } + public VehicleRegistrationRefDto { - nation = normalize(nation); + TachographNationRegistry.NationResolution nationResolution = + TachographNationRegistry.resolve(nation, nationNumericCode); + nation = normalize(nationResolution.legacyNation()); + nationNumericCode = nationResolution.numericCode(); number = normalizeNullable(number); } @@ -18,11 +28,19 @@ public record VehicleRegistrationRefDto( } public String stableKey() { - return (nation == null ? "" : nation) + ":" + (number == null ? "" : number); + String nationKey = nationNumericCode == null ? nation : String.valueOf(nationNumericCode); + return (nationKey == null ? "" : nationKey) + ":" + (number == null ? "" : number); } private static String normalize(String value) { - return value == null || value.isBlank() ? null : value.trim().toUpperCase(); + if (value == null || value.isBlank()) { + return null; + } + String trimmed = value.trim(); + if (trimmed.matches("(?i)^unknown\\s+[0-9]+$")) { + return "Unknown " + trimmed.replaceAll("[^0-9]", ""); + } + return trimmed.toUpperCase(); } private static String normalizeNullable(String value) { diff --git a/src/main/java/at/procon/eventhub/persistence/DriverIdentityRepository.java b/src/main/java/at/procon/eventhub/persistence/DriverIdentityRepository.java index bc0191e..3ba8187 100644 --- a/src/main/java/at/procon/eventhub/persistence/DriverIdentityRepository.java +++ b/src/main/java/at/procon/eventhub/persistence/DriverIdentityRepository.java @@ -40,10 +40,11 @@ public class DriverIdentityRepository { String sourceDriverEntityId = normalizeNullable(driverRef.sourceEntityId()); 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()); UUID driverId = findBySourceDriverEntityId(normalizedTenantKey, eventSourceId, sourceDriverEntityId); - UUID driverCardId = resolveOrCreateDriverCardId(cardNation, cardNumber, driverId); + UUID driverCardId = resolveOrCreateDriverCardId(cardNation, cardNationNumericCode, cardNumber, driverId); if (driverId == null && driverCardId != null) { driverId = findDriverIdByCardId(driverCardId); @@ -621,21 +622,24 @@ public class DriverIdentityRepository { private UUID resolveOrCreateDriverCardId( String cardNation, + Integer cardNationNumericCode, String cardNumber, UUID preferredDriverId ) { - if (cardNation == null || cardNumber == null) { + if ((cardNation == null && cardNationNumericCode == null) || cardNumber == null) { return null; } - UUID driverCardId = findDriverCardByCard(cardNation, cardNumber); + UUID driverCardId = findDriverCardByCard(cardNation, cardNationNumericCode, cardNumber); if (driverCardId == null) { Map payload = new LinkedHashMap<>(); put(payload, "source", "event"); put(payload, "card_nation", cardNation); + put(payload, "card_nation_numeric_code", cardNationNumericCode); put(payload, "card_number", cardNumber); return createDriverCard( preferredDriverId, cardNation, + cardNationNumericCode, cardNumber, null, payload @@ -683,10 +687,25 @@ public class DriverIdentityRepository { ); } - private UUID findDriverCardByCard(String cardNation, String cardNumber) { - if (cardNation == null || cardNumber == null) { + private UUID findDriverCardByCard(String cardNation, Integer cardNationNumericCode, String cardNumber) { + if ((cardNation == null && cardNationNumericCode == null) || cardNumber == null) { return null; } + if (cardNationNumericCode != null) { + return jdbcTemplate.query( + """ + select card.id + from eventhub.driver_card card + where card.nation_numeric_code = ? + and card.card_number = ? + order by card.updated_at desc + limit 1 + """, + rs -> rs.next() ? (UUID) rs.getObject("id") : null, + cardNationNumericCode, + cardNumber + ); + } return jdbcTemplate.query( """ select card.id @@ -782,6 +801,7 @@ public class DriverIdentityRepository { private UUID createDriverCard( UUID driverId, String cardNation, + Integer cardNationNumericCode, String cardNumber, OffsetDateTime sourceUpdatedAt, Map payload @@ -790,12 +810,13 @@ public class DriverIdentityRepository { jdbcTemplate.update( """ insert into eventhub.driver_card( - id, driver_id, nation, card_number, source_updated_at, payload, updated_at - ) values (?, ?, ?, ?, ?, ?::jsonb, now()) + id, driver_id, nation, nation_numeric_code, card_number, source_updated_at, payload, updated_at + ) values (?, ?, ?, ?, ?, ?, ?::jsonb, now()) """, driverCardId, driverId, cardNation, + cardNationNumericCode, cardNumber, sourceUpdatedAt, toJson(payload) diff --git a/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java b/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java index d3f4909..c4595fc 100644 --- a/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java +++ b/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java @@ -19,6 +19,7 @@ import at.procon.eventhub.dto.VehicleRefDto; import at.procon.eventhub.dto.VehicleRegistrationRefDto; import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest; import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; +import at.procon.eventhub.reference.TachographNationRegistry; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -149,11 +150,13 @@ public class EventHubEventReadRepository { detail.attributes, driver.source_driver_entity_id, driver_card.nation as driver_card_nation, + driver_card.nation_numeric_code as driver_card_nation_numeric_code, driver_card.card_number as driver_card_number, vehicle.source_vehicle_entity_id, vehicle.vin, registration.source_registration_entity_id, registration.nation as vehicle_registration_nation, + registration.nation_numeric_code as vehicle_registration_nation_numeric_code, registration.registration_number as vehicle_registration_number, event.driver_id, event.vehicle_id, @@ -209,8 +212,15 @@ public class EventHubEventReadRepository { sql.append(" and driver_card.card_number = ?"); params.add(driverCardNumber); if (driverCardNation != null) { - sql.append(" and driver_card.nation = ?"); - params.add(driverCardNation); + TachographNationRegistry.NationResolution driverCardNationResolution = + TachographNationRegistry.resolve(driverCardNation, null); + if (driverCardNationResolution.numericCode() != null) { + sql.append(" and driver_card.nation_numeric_code = ?"); + params.add(driverCardNationResolution.numericCode()); + } else { + sql.append(" and driver_card.nation = ?"); + params.add(driverCardNationResolution.legacyNation()); + } } } if (vehicleSourceEntityId != null) { @@ -225,8 +235,15 @@ public class EventHubEventReadRepository { sql.append(" and registration.registration_number = ?"); params.add(registrationNumber); if (registrationNation != null) { - sql.append(" and registration.nation = ?"); - params.add(registrationNation); + TachographNationRegistry.NationResolution registrationNationResolution = + TachographNationRegistry.resolve(registrationNation, null); + if (registrationNationResolution.numericCode() != null) { + sql.append(" and registration.nation_numeric_code = ?"); + params.add(registrationNationResolution.numericCode()); + } else { + sql.append(" and registration.nation = ?"); + params.add(registrationNationResolution.legacyNation()); + } } } @@ -275,7 +292,11 @@ public class EventHubEventReadRepository { String cardNumber = rs.getString("driver_card_number"); DriverCardRefDto driverCard = cardNumber == null ? null - : new DriverCardRefDto(rs.getString("driver_card_nation"), cardNumber); + : new DriverCardRefDto( + rs.getString("driver_card_nation"), + integerValue(rs, "driver_card_nation_numeric_code"), + cardNumber + ); DriverRefDto driverRef = new DriverRefDto(sourceEntityId, driverCard); return driverRef.hasAnyReference() ? driverRef : null; } @@ -284,7 +305,11 @@ public class EventHubEventReadRepository { VehicleRegistrationRefDto registration = null; String registrationNumber = rs.getString("vehicle_registration_number"); if (registrationNumber != null) { - registration = new VehicleRegistrationRefDto(rs.getString("vehicle_registration_nation"), registrationNumber); + registration = new VehicleRegistrationRefDto( + rs.getString("vehicle_registration_nation"), + integerValue(rs, "vehicle_registration_nation_numeric_code"), + registrationNumber + ); } VehicleRefDto vehicleRef = new VehicleRefDto( firstNonBlank( @@ -406,6 +431,17 @@ public class EventHubEventReadRepository { return Long.parseLong(value.toString()); } + private Integer integerValue(ResultSet rs, String column) throws SQLException { + Object value = rs.getObject(column); + if (value == null) { + return null; + } + if (value instanceof Number number) { + return number.intValue(); + } + return Integer.parseInt(value.toString()); + } + private JsonNode json(String value) { try { return value == null || value.isBlank() diff --git a/src/main/java/at/procon/eventhub/persistence/EventRepository.java b/src/main/java/at/procon/eventhub/persistence/EventRepository.java index 87dbfb9..1f3d0e0 100644 --- a/src/main/java/at/procon/eventhub/persistence/EventRepository.java +++ b/src/main/java/at/procon/eventhub/persistence/EventRepository.java @@ -532,14 +532,18 @@ public class EventRepository { String vin = normalizeNullable(vehicleRef.vin()); String sourceRegistrationEntityId = normalizeNullable(vehicleRef.sourceRegistrationEntityId()); VehicleRegistrationRefDto registration = vehicleRef.vehicleRegistration(); - String registrationNation = registration == null ? null : normalizeNullable(registration.nation()); + String registrationNationKey = registration == null + ? null + : registration.nationNumericCode() == null + ? normalizeNullable(registration.nation()) + : registration.nationNumericCode().toString(); String registrationNumber = registration == null ? null : normalizeNullable(registration.number()); return String.join("|", sourceVehicleEntityId == null ? "" : sourceVehicleEntityId, vin == null ? "" : vin, sourceRegistrationEntityId == null ? "" : sourceRegistrationEntityId, - registrationNation == null ? "" : registrationNation, + registrationNationKey == null ? "" : registrationNationKey, registrationNumber == null ? "" : registrationNumber ); } diff --git a/src/main/java/at/procon/eventhub/persistence/VehicleIdentityRepository.java b/src/main/java/at/procon/eventhub/persistence/VehicleIdentityRepository.java index a7be805..70981be 100644 --- a/src/main/java/at/procon/eventhub/persistence/VehicleIdentityRepository.java +++ b/src/main/java/at/procon/eventhub/persistence/VehicleIdentityRepository.java @@ -41,6 +41,7 @@ public class VehicleIdentityRepository { String sourceRegistrationEntityId = normalizeNullable(vehicleRef.sourceRegistrationEntityId()); VehicleRegistrationRefDto registration = vehicleRef.vehicleRegistration(); String registrationNation = registration == null ? null : normalizeNullable(registration.nation()); + Integer registrationNationNumericCode = registration == null ? null : registration.nationNumericCode(); String registrationNumber = registration == null ? null : normalizeNullable(registration.number()); String registrationNationForCreate = creationNation(registrationNation, registrationNumber); @@ -49,14 +50,21 @@ public class VehicleIdentityRepository { eventSourceId, sourceRegistrationEntityId, registrationNation, + registrationNationNumericCode, registrationNumber ); if (registrationId == null && (sourceRegistrationEntityId != null || registrationNumber != null)) { registrationId = createRegistration( registrationNationForCreate, + registrationNationNumericCode, registrationNumber, null, - Map.of("source", "event") + Map.of( + "source", "event", + "registration_nation", registrationNationForCreate, + "registration_nation_numeric_code", registrationNationNumericCode, + "registration_number", registrationNumber + ) ); } if (registrationId != null && sourceRegistrationEntityId != null) { @@ -95,7 +103,7 @@ public class VehicleIdentityRepository { ); } touchVehicle(vehicleId, vin); - touchRegistration(registrationId, registrationNationForCreate, registrationNumber); + touchRegistration(registrationId, registrationNationForCreate, registrationNationNumericCode, registrationNumber); return new ResolvedVehicleReferenceResolution( new ResolvedVehicleReference(vehicleId, registrationId), @@ -617,6 +625,7 @@ public class VehicleIdentityRepository { int eventSourceId, String sourceRegistrationEntityId, String nation, + Integer nationNumericCode, String registrationNumber ) { if (isYellowFoxSyntheticRegistrationNation(nation) && registrationNumber != null) { @@ -628,7 +637,7 @@ public class VehicleIdentityRepository { } UUID registrationId = findRegistrationBySourceRegistrationEntityId(tenantKey, eventSourceId, sourceRegistrationEntityId); if (registrationId == null) { - registrationId = findRegistrationByPlate(nation, registrationNumber); + registrationId = findRegistrationByPlate(nation, nationNumericCode, registrationNumber); } return registrationId; } @@ -696,10 +705,25 @@ public class VehicleIdentityRepository { ); } - private UUID findRegistrationByPlate(String nation, String registrationNumber) { - if (nation == null || registrationNumber == null) { + private UUID findRegistrationByPlate(String nation, Integer nationNumericCode, String registrationNumber) { + if ((nation == null && nationNumericCode == null) || registrationNumber == null) { return null; } + if (nationNumericCode != null) { + return jdbcTemplate.query( + """ + select r.id + from eventhub.vehicle_registration r + where r.nation_numeric_code = ? + and r.registration_number = ? + order by r.updated_at desc + limit 1 + """, + rs -> rs.next() ? (UUID) rs.getObject("id") : null, + nationNumericCode, + registrationNumber + ); + } return jdbcTemplate.query( """ select r.id @@ -787,6 +811,7 @@ public class VehicleIdentityRepository { private UUID createRegistration( String nation, + Integer nationNumericCode, String registrationNumber, OffsetDateTime sourceUpdatedAt, Map payload @@ -798,11 +823,12 @@ public class VehicleIdentityRepository { jdbcTemplate.update( """ insert into eventhub.vehicle_registration( - id, nation, registration_number, source_updated_at, payload, updated_at - ) values (?, ?, ?, ?, ?::jsonb, now()) + id, nation, nation_numeric_code, registration_number, source_updated_at, payload, updated_at + ) values (?, ?, ?, ?, ?, ?::jsonb, now()) """, registrationId, nation, + nationNumericCode, registrationNumber, sourceUpdatedAt, toJson(payload) @@ -946,26 +972,30 @@ public class VehicleIdentityRepository { ); } - private void touchRegistration(UUID registrationId, String nation, String registrationNumber) { - if (registrationId == null || (nation == null && registrationNumber == null)) { + private void touchRegistration(UUID registrationId, String nation, Integer nationNumericCode, String registrationNumber) { + if (registrationId == null || (nation == null && nationNumericCode == null && registrationNumber == null)) { return; } jdbcTemplate.update( """ update eventhub.vehicle_registration set nation = coalesce(cast(? as text), nation), + nation_numeric_code = coalesce(cast(? as integer), nation_numeric_code), registration_number = coalesce(cast(? as text), registration_number), updated_at = now() where id = ? and ( (nation is null and cast(? as text) is not null) + or (nation_numeric_code is null and cast(? as integer) is not null) or (registration_number is null and cast(? as text) is not null) ) """, nation, + nationNumericCode, registrationNumber, registrationId, nation, + nationNumericCode, registrationNumber ); } diff --git a/src/main/java/at/procon/eventhub/processing/service/TachographDbRuntimeEventLoader.java b/src/main/java/at/procon/eventhub/processing/service/TachographDbRuntimeEventLoader.java index a357117..07dc1c0 100644 --- a/src/main/java/at/procon/eventhub/processing/service/TachographDbRuntimeEventLoader.java +++ b/src/main/java/at/procon/eventhub/processing/service/TachographDbRuntimeEventLoader.java @@ -15,6 +15,7 @@ import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; +import at.procon.eventhub.reference.TachographNationRegistry; import at.procon.eventhub.tachograph.dto.TachographImportRequest; import at.procon.eventhub.tachograph.service.TachographExtractionDefinitionRegistry; import java.io.IOException; @@ -199,11 +200,11 @@ public class TachographDbRuntimeEventLoader implements RuntimeDriverEventLoader, params.put("lastSourcePackageImportedAt", null); params.put("lastSourcePackageIdNumeric", null); params.put("driverSourceEntityId", request.driverSourceEntityId()); - params.put("driverCardNation", request.driverCardNation()); + params.put("driverCardNation", alphaNation(request.driverCardNation())); params.put("driverCardNumber", request.driverCardNumber()); params.put("vehicleSourceEntityId", vehicleRef == null ? null : vehicleRef.sourceVehicleEntityId()); params.put("vehicleVin", vehicleRef == null ? null : vehicleRef.vin()); - params.put("vehicleRegistrationNation", vehicleRef == null ? null : vehicleRef.registrationNation()); + params.put("vehicleRegistrationNation", vehicleRef == null ? null : alphaNation(vehicleRef.registrationNation())); params.put("vehicleRegistrationNumber", vehicleRef == null ? null : vehicleRef.registrationNumber()); return params; } @@ -251,4 +252,8 @@ public class TachographDbRuntimeEventLoader implements RuntimeDriverEventLoader, throw new IllegalStateException("Failed to load tachograph runtime SQL " + sqlResource, ex); } } + + private String alphaNation(String rawNation) { + return TachographNationRegistry.alphaCode(rawNation, null); + } } diff --git a/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java index 8bc0478..8b1ee03 100644 --- a/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java +++ b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java @@ -4,6 +4,7 @@ import at.procon.eventhub.dto.EventHubEventDto; import at.procon.eventhub.dto.VehicleRefDto; import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; +import at.procon.eventhub.reference.TachographNationRegistry; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import at.procon.eventhub.tachographfilesession.service.DriverTimelineEventBuilder; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException; @@ -57,8 +58,7 @@ public class TachographFileSessionUnifiedVehicleEventSource implements UnifiedVe return request.registrationNumber() != null && vehicleRef.vehicleRegistration() != null && request.registrationNumber().equals(vehicleRef.vehicleRegistration().number()) - && (request.registrationNation() == null - || request.registrationNation().equals(vehicleRef.vehicleRegistration().nation())); + && matchesNation(request.registrationNation(), vehicleRef.vehicleRegistration().nation(), vehicleRef.vehicleRegistration().nationNumericCode()); } private boolean withinWindow( @@ -74,4 +74,18 @@ public class TachographFileSessionUnifiedVehicleEventSource implements UnifiedVe } return occurredTo == null || !occurredAt.isAfter(occurredTo); } + + private boolean matchesNation(String requestedNation, String actualNation, Integer actualNationNumericCode) { + if (requestedNation == null) { + return true; + } + TachographNationRegistry.NationResolution requested = TachographNationRegistry.resolve(requestedNation, null); + TachographNationRegistry.NationResolution actual = TachographNationRegistry.resolve(actualNation, actualNationNumericCode); + if (requested.numericCode() != null || actual.numericCode() != null) { + return requested.numericCode() != null + && actual.numericCode() != null + && requested.numericCode().equals(actual.numericCode()); + } + return requested.legacyNation() != null && requested.legacyNation().equals(actual.legacyNation()); + } } diff --git a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyService.java b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyService.java index 0b03889..909b92d 100644 --- a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyService.java +++ b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyService.java @@ -97,7 +97,11 @@ public class UnifiedRuntimeEventAssemblyService { UnifiedDiscoveredVehicleRef candidate = new UnifiedDiscoveredVehicleRef( vehicleRef.sourceVehicleEntityId(), vehicleRef.vin(), - vehicleRef.vehicleRegistration() == null ? null : vehicleRef.vehicleRegistration().nation(), + vehicleRef.vehicleRegistration() == null + ? null + : vehicleRef.vehicleRegistration().nationNumericCode() == null + ? vehicleRef.vehicleRegistration().nation() + : vehicleRef.vehicleRegistration().nationNumericCode().toString(), vehicleRef.vehicleRegistration() == null ? null : vehicleRef.vehicleRegistration().number() ); if (!candidate.hasAnyReference()) { diff --git a/src/main/java/at/procon/eventhub/reference/TachographNationRegistry.java b/src/main/java/at/procon/eventhub/reference/TachographNationRegistry.java new file mode 100644 index 0000000..668a0d5 --- /dev/null +++ b/src/main/java/at/procon/eventhub/reference/TachographNationRegistry.java @@ -0,0 +1,178 @@ +package at.procon.eventhub.reference; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class TachographNationRegistry { + + private static final Pattern UNKNOWN_NUMERIC_PATTERN = Pattern.compile("(?i)^unknown\\s+([0-9]+)$"); + private static final String RESOURCE_PATH = "/reference/nation.csv"; + private static final Map BY_NUMERIC_CODE; + private static final Map BY_ALPHA_CODE; + + static { + Map byNumeric = new LinkedHashMap<>(); + Map byAlpha = new LinkedHashMap<>(); + try { + var inputStream = TachographNationRegistry.class.getResourceAsStream(RESOURCE_PATH); + if (inputStream == null) { + throw new IllegalStateException("Missing tachograph nation reference data " + RESOURCE_PATH); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + reader.readLine(); + String line; + while ((line = reader.readLine()) != null) { + if (line.isBlank()) { + continue; + } + String[] parts = line.split(";", -1); + if (parts.length < 5) { + continue; + } + Integer numericCode = parseInteger(parts[3]); + if (numericCode == null) { + continue; + } + String name = normalizeNullable(parts[1]); + String alphaCode = normalizeAlpha(parts[2]); + String defaultLanguageCode = normalizeNullable(parts[4]); + NationRecord record = new NationRecord(numericCode, alphaCode, name, defaultLanguageCode, true); + byNumeric.put(numericCode, record); + if (alphaCode != null) { + byAlpha.put(alphaCode, record); + } + } + } + } catch (IOException ex) { + throw new IllegalStateException("Failed to load tachograph nation reference data.", ex); + } + BY_NUMERIC_CODE = Collections.unmodifiableMap(byNumeric); + BY_ALPHA_CODE = Collections.unmodifiableMap(byAlpha); + } + + private TachographNationRegistry() { + } + + public static NationResolution resolve(String legacyNation, Integer nationNumericCode) { + String normalizedLegacyNation = normalizeNullable(legacyNation); + if (nationNumericCode != null) { + NationRecord record = BY_NUMERIC_CODE.get(nationNumericCode); + if (record != null) { + return new NationResolution( + record.alphaCode(), + record.numericCode(), + record.name(), + true + ); + } + return new NationResolution( + normalizedLegacyNation != null ? normalizedLegacyNation : unknownLabel(nationNumericCode), + nationNumericCode, + unknownLabel(nationNumericCode), + false + ); + } + if (normalizedLegacyNation == null) { + return NationResolution.empty(); + } + Integer parsedNumericCode = parseInteger(normalizedLegacyNation); + if (parsedNumericCode != null) { + return resolve(null, parsedNumericCode); + } + Matcher unknownMatcher = UNKNOWN_NUMERIC_PATTERN.matcher(normalizedLegacyNation); + if (unknownMatcher.matches()) { + Integer unknownNumericCode = parseInteger(unknownMatcher.group(1)); + if (unknownNumericCode != null) { + return resolve(normalizedLegacyNation, unknownNumericCode); + } + } + NationRecord record = BY_ALPHA_CODE.get(normalizedLegacyNation.toUpperCase(Locale.ROOT)); + if (record != null) { + return new NationResolution( + record.alphaCode(), + record.numericCode(), + record.name(), + true + ); + } + return new NationResolution(normalizedLegacyNation, null, null, false); + } + + public static Integer numericCode(String legacyNation, Integer nationNumericCode) { + return resolve(legacyNation, nationNumericCode).numericCode(); + } + + public static String alphaCode(String legacyNation, Integer nationNumericCode) { + return resolve(legacyNation, nationNumericCode).legacyNation(); + } + + public static NationRecord recordByNumericCode(Integer numericCode) { + if (numericCode == null) { + return null; + } + NationRecord record = BY_NUMERIC_CODE.get(numericCode); + return record != null + ? record + : new NationRecord(numericCode, null, unknownLabel(numericCode), null, false); + } + + private static String normalizeNullable(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + if (trimmed.equalsIgnoreCase("NULL")) { + return null; + } + return trimmed.isEmpty() ? null : trimmed; + } + + private static String normalizeAlpha(String value) { + String normalized = normalizeNullable(value); + return normalized == null ? null : normalized.toUpperCase(Locale.ROOT); + } + + private static Integer parseInteger(String value) { + String normalized = normalizeNullable(value); + if (normalized == null) { + return null; + } + try { + return Integer.parseInt(normalized); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static String unknownLabel(Integer numericCode) { + return numericCode == null ? null : "Unknown " + numericCode; + } + + public record NationRecord( + Integer numericCode, + String alphaCode, + String name, + String defaultLanguageCode, + boolean known + ) { + } + + public record NationResolution( + String legacyNation, + Integer numericCode, + String displayName, + boolean known + ) { + public static NationResolution empty() { + return new NationResolution(null, null, null, false); + } + } +} diff --git a/src/main/java/at/procon/eventhub/service/EventDetailsFactory.java b/src/main/java/at/procon/eventhub/service/EventDetailsFactory.java index df783c8..2db4edd 100644 --- a/src/main/java/at/procon/eventhub/service/EventDetailsFactory.java +++ b/src/main/java/at/procon/eventhub/service/EventDetailsFactory.java @@ -5,6 +5,7 @@ import at.procon.eventhub.dto.CardStatus; import at.procon.eventhub.dto.DriverCardRefDto; import at.procon.eventhub.dto.DrivingStatus; import at.procon.eventhub.dto.EventDetailsDto; +import at.procon.eventhub.reference.TachographNationRegistry; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.math.BigDecimal; @@ -39,6 +40,7 @@ public class EventDetailsFactory { put(attributes, "cardStatus", cardStatus); if (driverCard != null && driverCard.hasValue()) { put(attributes, "cardNation", driverCard.nation()); + put(attributes, "cardNationNumericCode", driverCard.nationNumericCode()); put(attributes, "cardNumber", driverCard.number()); } return new EventDetailsDto("DRIVER_CARD", objectMapper.valueToTree(attributes)); @@ -52,15 +54,21 @@ public class EventDetailsFactory { public EventDetailsDto place(String country, String region) { Map attributes = new LinkedHashMap<>(); - put(attributes, "country", country); + TachographNationRegistry.NationResolution nationResolution = TachographNationRegistry.resolve(country, null); + put(attributes, "country", nationResolution.legacyNation()); + put(attributes, "countryNumericCode", nationResolution.numericCode()); put(attributes, "region", region); return new EventDetailsDto("PLACE", objectMapper.valueToTree(attributes)); } public EventDetailsDto borderCrossing(String countryFrom, String countryTo) { Map attributes = new LinkedHashMap<>(); - put(attributes, "countryFrom", countryFrom); - put(attributes, "countryTo", countryTo); + TachographNationRegistry.NationResolution fromResolution = TachographNationRegistry.resolve(countryFrom, null); + TachographNationRegistry.NationResolution toResolution = TachographNationRegistry.resolve(countryTo, null); + put(attributes, "countryFrom", fromResolution.legacyNation()); + put(attributes, "countryFromNumericCode", fromResolution.numericCode()); + put(attributes, "countryTo", toResolution.legacyNation()); + put(attributes, "countryToNumericCode", toResolution.numericCode()); return new EventDetailsDto("BORDER_CROSSING", objectMapper.valueToTree(attributes)); } diff --git a/src/main/resources/db/eventhub_schema_create.sql b/src/main/resources/db/eventhub_schema_create.sql index adeb274..ddbb563 100644 --- a/src/main/resources/db/eventhub_schema_create.sql +++ b/src/main/resources/db/eventhub_schema_create.sql @@ -145,6 +145,84 @@ create table if not exists eventhub.source_master_relation ( constraint chk_source_master_relation_valid_time_order check (valid_from is null or valid_to is null or valid_from <= valid_to) ); +create table if not exists eventhub.nation ( + numeric_code integer primary key, + alpha_code text, + name text not null, + default_language_code text, + is_known boolean not null default true, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +insert into eventhub.nation(numeric_code, alpha_code, name, default_language_code, is_known) +values + (0, '0', 'Unknown 0', null, true), + (1, 'A', 'Austria', 'de-AT', true), + (2, 'AL', 'Albania', 'sq-AL', true), + (3, 'AND', 'Andorra', 'ca', true), + (4, 'ARM', 'Armenia', 'hy-AM', true), + (5, 'AZ', 'Azerbaijan', 'az', true), + (6, 'B', 'Belgium', null, true), + (7, 'BG', 'Bulgaria', 'bg-BG', true), + (8, 'BIH', 'Bosnia Herzegovina', null, true), + (9, 'BY', 'Belarus', 'be-BY', true), + (10, 'CH', 'Switzerland', 'de-CH', true), + (11, 'CY', 'Cyprus', null, true), + (12, 'CZ', 'Czech Republic', 'cs-CZ', true), + (13, 'D', 'Germany', 'de-DE', true), + (14, 'DK', 'Denmark', 'da-DK', true), + (15, 'E', 'Spain', 'es-ES', true), + (16, 'EST', 'Estonia', 'et-EE', true), + (17, 'F', 'France', 'fr-FR', true), + (18, 'FIN', 'Finland', 'fi-FI', true), + (19, 'FL', 'Liechtenstein', 'de-LI', true), + (20, 'FR', 'Faroe Islands', 'fo-FO', true), + (21, 'UK', 'United Kingdom', 'en-GB', true), + (22, 'GE', 'Georgia', 'ka-GE', true), + (23, 'GR', 'Greece', 'el-GR', true), + (24, 'H', 'Hungary', 'hu-HU', true), + (25, 'HR', 'Croatia', 'hr-HR', true), + (26, 'I', 'Italy', 'it-IT', true), + (27, 'IRL', 'Ireland', 'en-IE', true), + (28, 'IS', 'Iceland', 'is-IS', true), + (29, 'KZ', 'Kazakhstan', 'kk-KZ', true), + (30, 'L', 'Luxembourg', 'de-LU', true), + (31, 'LT', 'Lithuania', 'lt-LT', true), + (32, 'LV', 'Latvia', 'lv-LV', true), + (33, 'M', 'Malta', null, true), + (34, 'MC', 'Monaco', 'fr-MC', true), + (35, 'MD', 'Moldova', 'ro', true), + (36, 'MK', 'North Macedonia', 'mk-MK', true), + (37, 'N', 'Norway', 'no', true), + (38, 'NL', 'Netherlands', 'nl-NL', true), + (39, 'P', 'Portugal', 'pt-PT', true), + (40, 'PL', 'Poland', 'pl-PL', true), + (41, 'RO', 'Romania', 'ro-RO', true), + (42, 'RSM', 'San Marino', 'it-IT', true), + (43, 'RUS', 'Russia', 'ru-RU', true), + (44, 'S', 'Sweden', 'sv-SE', true), + (45, 'SK', 'Slovakia', 'sk-SK', true), + (46, 'SLO', 'Slovenia', 'sl-SI', true), + (47, 'TM', 'Turkmenistan', null, true), + (48, 'TR', 'Turkey', 'tr-TR', true), + (49, 'UA', 'Ukraine', 'uk-UA', true), + (50, 'V', 'Vatican City', 'it-IT', true), + (51, 'YU', 'Yugoslavia', null, true), + (52, 'MNE', 'Montenegro', null, true), + (53, 'SRB', 'Serbia', null, true), + (54, 'UZ', 'Uzbekistan', null, true), + (55, 'TJ', 'Tajikistan', null, true), + (253, 'EC', 'European Community', null, true), + (254, 'EUR', 'Rest of Europe', null, true), + (255, 'WLD', 'Rest of the world', null, true) +on conflict (numeric_code) do update set + alpha_code = excluded.alpha_code, + name = excluded.name, + default_language_code = excluded.default_language_code, + is_known = excluded.is_known, + updated_at = now(); + create table if not exists eventhub.driver ( id uuid primary key, first_names text, @@ -160,6 +238,7 @@ create table if not exists eventhub.driver_card ( id uuid primary key, driver_id uuid references eventhub.driver(id), nation text not null, + nation_numeric_code integer references eventhub.nation(numeric_code), card_number text not null, source_updated_at timestamptz, payload jsonb not null default '{}'::jsonb, @@ -203,6 +282,7 @@ create table if not exists eventhub.vehicle ( create table if not exists eventhub.vehicle_registration ( id uuid primary key, nation text not null, + nation_numeric_code integer references eventhub.nation(numeric_code), registration_number text not null, source_updated_at timestamptz, payload jsonb not null default '{}'::jsonb, @@ -353,6 +433,10 @@ create index if not exists idx_vehicle_vin create index if not exists idx_vehicle_registration_plate on eventhub.vehicle_registration(nation, registration_number); +create index if not exists idx_vehicle_registration_numeric_key + on eventhub.vehicle_registration(nation_numeric_code, registration_number) + where nation_numeric_code is not null; + create index if not exists idx_vehicle_registration_assignment_registration_time on eventhub.vehicle_registration_assignment(vehicle_registration_id, valid_from desc, valid_to); @@ -382,6 +466,10 @@ create index if not exists idx_event_domain_type_time create index if not exists idx_driver_card_key on eventhub.driver_card(nation, card_number); +create index if not exists idx_driver_card_numeric_key + on eventhub.driver_card(nation_numeric_code, card_number) + where nation_numeric_code is not null; + create index if not exists idx_driver_card_driver on eventhub.driver_card(driver_id) where driver_id is not null; @@ -396,6 +484,10 @@ create unique index if not exists ux_vehicle_vin create unique index if not exists ux_vehicle_registration_plate on eventhub.vehicle_registration(nation, registration_number); +create unique index if not exists ux_eventhub_nation_alpha_code + on eventhub.nation(alpha_code) + where alpha_code is not null; + create index if not exists idx_source_driver_identity_driver on eventhub.source_driver_identity(driver_id); diff --git a/src/main/resources/db/migration/V13__add_nation_reference_and_numeric_codes.sql b/src/main/resources/db/migration/V13__add_nation_reference_and_numeric_codes.sql new file mode 100644 index 0000000..80ad137 --- /dev/null +++ b/src/main/resources/db/migration/V13__add_nation_reference_and_numeric_codes.sql @@ -0,0 +1,147 @@ +create table if not exists eventhub.nation ( + numeric_code integer primary key, + alpha_code text, + name text not null, + default_language_code text, + is_known boolean not null default true, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create unique index if not exists ux_eventhub_nation_alpha_code + on eventhub.nation(alpha_code) + where alpha_code is not null; + +insert into eventhub.nation(numeric_code, alpha_code, name, default_language_code, is_known) +values + (0, '0', 'Unknown 0', null, true), + (1, 'A', 'Austria', 'de-AT', true), + (2, 'AL', 'Albania', 'sq-AL', true), + (3, 'AND', 'Andorra', 'ca', true), + (4, 'ARM', 'Armenia', 'hy-AM', true), + (5, 'AZ', 'Azerbaijan', 'az', true), + (6, 'B', 'Belgium', null, true), + (7, 'BG', 'Bulgaria', 'bg-BG', true), + (8, 'BIH', 'Bosnia Herzegovina', null, true), + (9, 'BY', 'Belarus', 'be-BY', true), + (10, 'CH', 'Switzerland', 'de-CH', true), + (11, 'CY', 'Cyprus', null, true), + (12, 'CZ', 'Czech Republic', 'cs-CZ', true), + (13, 'D', 'Germany', 'de-DE', true), + (14, 'DK', 'Denmark', 'da-DK', true), + (15, 'E', 'Spain', 'es-ES', true), + (16, 'EST', 'Estonia', 'et-EE', true), + (17, 'F', 'France', 'fr-FR', true), + (18, 'FIN', 'Finland', 'fi-FI', true), + (19, 'FL', 'Liechtenstein', 'de-LI', true), + (20, 'FR', 'Faroe Islands', 'fo-FO', true), + (21, 'UK', 'United Kingdom', 'en-GB', true), + (22, 'GE', 'Georgia', 'ka-GE', true), + (23, 'GR', 'Greece', 'el-GR', true), + (24, 'H', 'Hungary', 'hu-HU', true), + (25, 'HR', 'Croatia', 'hr-HR', true), + (26, 'I', 'Italy', 'it-IT', true), + (27, 'IRL', 'Ireland', 'en-IE', true), + (28, 'IS', 'Iceland', 'is-IS', true), + (29, 'KZ', 'Kazakhstan', 'kk-KZ', true), + (30, 'L', 'Luxembourg', 'de-LU', true), + (31, 'LT', 'Lithuania', 'lt-LT', true), + (32, 'LV', 'Latvia', 'lv-LV', true), + (33, 'M', 'Malta', null, true), + (34, 'MC', 'Monaco', 'fr-MC', true), + (35, 'MD', 'Moldova', 'ro', true), + (36, 'MK', 'North Macedonia', 'mk-MK', true), + (37, 'N', 'Norway', 'no', true), + (38, 'NL', 'Netherlands', 'nl-NL', true), + (39, 'P', 'Portugal', 'pt-PT', true), + (40, 'PL', 'Poland', 'pl-PL', true), + (41, 'RO', 'Romania', 'ro-RO', true), + (42, 'RSM', 'San Marino', 'it-IT', true), + (43, 'RUS', 'Russia', 'ru-RU', true), + (44, 'S', 'Sweden', 'sv-SE', true), + (45, 'SK', 'Slovakia', 'sk-SK', true), + (46, 'SLO', 'Slovenia', 'sl-SI', true), + (47, 'TM', 'Turkmenistan', null, true), + (48, 'TR', 'Turkey', 'tr-TR', true), + (49, 'UA', 'Ukraine', 'uk-UA', true), + (50, 'V', 'Vatican City', 'it-IT', true), + (51, 'YU', 'Yugoslavia', null, true), + (52, 'MNE', 'Montenegro', null, true), + (53, 'SRB', 'Serbia', null, true), + (54, 'UZ', 'Uzbekistan', null, true), + (55, 'TJ', 'Tajikistan', null, true), + (253, 'EC', 'European Community', null, true), + (254, 'EUR', 'Rest of Europe', null, true), + (255, 'WLD', 'Rest of the world', null, true) +on conflict (numeric_code) do update set + alpha_code = excluded.alpha_code, + name = excluded.name, + default_language_code = excluded.default_language_code, + is_known = excluded.is_known, + updated_at = now(); + +alter table eventhub.driver_card + add column if not exists nation_numeric_code integer references eventhub.nation(numeric_code); + +alter table eventhub.vehicle_registration + add column if not exists nation_numeric_code integer references eventhub.nation(numeric_code); + +create index if not exists idx_driver_card_numeric_key + on eventhub.driver_card(nation_numeric_code, card_number) + where nation_numeric_code is not null; + +create index if not exists idx_vehicle_registration_numeric_key + on eventhub.vehicle_registration(nation_numeric_code, registration_number) + where nation_numeric_code is not null; + +update eventhub.driver_card card +set nation_numeric_code = nation_ref.numeric_code +from eventhub.nation nation_ref +where card.nation_numeric_code is null + and upper(card.nation) = upper(nation_ref.alpha_code); + +update eventhub.driver_card card +set nation_numeric_code = cast(card.nation as integer) +where card.nation_numeric_code is null + and trim(card.nation) ~ '^[0-9]+$'; + +update eventhub.driver_card card +set nation_numeric_code = cast(substring(card.nation from '^[Uu]nknown[[:space:]]+([0-9]+)$') as integer) +where card.nation_numeric_code is null + and substring(card.nation from '^[Uu]nknown[[:space:]]+([0-9]+)$') is not null; + +update eventhub.driver_card card +set nation = coalesce(nation_ref.alpha_code, 'Unknown ' || card.nation_numeric_code::text) +from eventhub.nation nation_ref +where card.nation_numeric_code is not null + and nation_ref.numeric_code = card.nation_numeric_code + and ( + trim(card.nation) ~ '^[0-9]+$' + or substring(card.nation from '^[Uu]nknown[[:space:]]+([0-9]+)$') is not null + ); + +update eventhub.vehicle_registration registration +set nation_numeric_code = nation_ref.numeric_code +from eventhub.nation nation_ref +where registration.nation_numeric_code is null + and upper(registration.nation) = upper(nation_ref.alpha_code); + +update eventhub.vehicle_registration registration +set nation_numeric_code = cast(registration.nation as integer) +where registration.nation_numeric_code is null + and trim(registration.nation) ~ '^[0-9]+$'; + +update eventhub.vehicle_registration registration +set nation_numeric_code = cast(substring(registration.nation from '^[Uu]nknown[[:space:]]+([0-9]+)$') as integer) +where registration.nation_numeric_code is null + and substring(registration.nation from '^[Uu]nknown[[:space:]]+([0-9]+)$') is not null; + +update eventhub.vehicle_registration registration +set nation = coalesce(nation_ref.alpha_code, 'Unknown ' || registration.nation_numeric_code::text) +from eventhub.nation nation_ref +where registration.nation_numeric_code is not null + and nation_ref.numeric_code = registration.nation_numeric_code + and ( + trim(registration.nation) ~ '^[0-9]+$' + or substring(registration.nation from '^[Uu]nknown[[:space:]]+([0-9]+)$') is not null + ); diff --git a/src/main/resources/reference/nation.csv b/src/main/resources/reference/nation.csv new file mode 100644 index 0000000..cb10f4b --- /dev/null +++ b/src/main/resources/reference/nation.csv @@ -0,0 +1,60 @@ +ID;Name;AlphaCode;NumericCode;DefaultLanguageCode;ID_Certificate;ID_FileLog +0;Unknown 0;0;0;NULL;NULL;NULL +1;Austria;A;1;de-AT;NULL;NULL +2;Albania;AL;2;sq-AL;NULL;NULL +3;Andorra;AND;3;ca;NULL;NULL +4;Armenia;ARM;4;hy-AM;NULL;NULL +5;Azerbaijan;AZ;5;az;NULL;NULL +6;Belgium;B;6;NULL;NULL;NULL +7;Bulgaria;BG;7;bg-BG;NULL;NULL +8;Bosnia Herzegovina;BIH;8;NULL;NULL;NULL +9;Belarus;BY;9;be-BY;NULL;NULL +10;Switzerland;CH;10;de-CH;NULL;NULL +11;Cyprus;CY;11;NULL;NULL;NULL +12;Czech Republic;CZ;12;cs-CZ;NULL;NULL +13;Germany;D;13;de-DE;NULL;NULL +14;Denmark;DK;14;da-DK;NULL;NULL +15;Spain;E;15;es-ES;NULL;NULL +16;Estonia;EST;16;et-EE;NULL;NULL +17;France;F;17;fr-FR;NULL;NULL +18;Finland;FIN;18;fi-FI;NULL;NULL +19;Liechtenstein;FL;19;de-LI;NULL;NULL +20;Faroe Islands;FR;20;fo-FO;NULL;NULL +21;United Kingdom;UK;21;en-GB;NULL;NULL +22;Georgia;GE;22;ka-GE;NULL;NULL +23;Greece;GR;23;el-GR;NULL;NULL +24;Hungary;H;24;hu-HU;NULL;NULL +25;Croatia;HR;25;hr-HR;NULL;NULL +26;Italy;I;26;it-IT;NULL;NULL +27;Ireland;IRL;27;en-IE;NULL;NULL +28;Iceland;IS;28;is-IS;NULL;NULL +29;Kazakhstan;KZ;29;kk-KZ;NULL;NULL +30;Luxembourg;L;30;de-LU;NULL;NULL +31;Lithuania;LT;31;lt-LT;NULL;NULL +32;Latvia;LV;32;lv-LV;NULL;NULL +33;Malta;M;33;NULL;NULL;NULL +34;Monaco;MC;34;fr-MC;NULL;NULL +35;Moldova;MD;35;ro;NULL;NULL +36;North Macedonia;MK;36;mk-MK;NULL;NULL +37;Norway;N;37;no;NULL;NULL +38;Netherlands;NL;38;nl-NL;NULL;NULL +39;Portugal;P;39;pt-PT;NULL;NULL +40;Poland;PL;40;pl-PL;NULL;NULL +41;Romania;RO;41;ro-RO;NULL;NULL +42;San Marino;RSM;42;it-IT;NULL;NULL +43;Russia;RUS;43;ru-RU;NULL;NULL +44;Sweden;S;44;sv-SE;NULL;NULL +45;Slovakia;SK;45;sk-SK;NULL;NULL +46;Slovenia;SLO;46;sl-SI;NULL;NULL +47;Turkmenistan;TM;47;NULL;NULL;NULL +48;Turkey;TR;48;tr-TR;NULL;NULL +49;Ukraine;UA;49;uk-UA;NULL;NULL +50;Vatican City;V;50;it-IT;NULL;NULL +51;Yugoslavia;YU;51;NULL;NULL;NULL +52;Montenegro;MNE;52;NULL;NULL;763087 +53;Serbia;SRB;53;NULL;NULL;272576 +54;Uzbekistan;UZ;54;NULL;NULL;308001 +55;Tajikistan;TJ;55;NULL;NULL;NULL +253;European Community;EC;253;NULL;NULL;NULL +254;Rest of Europe;EUR;254;NULL;NULL;NULL +255;Rest of the world;WLD;255;NULL;NULL;NULL diff --git a/src/test/java/at/procon/eventhub/reference/TachographNationRegistryTest.java b/src/test/java/at/procon/eventhub/reference/TachographNationRegistryTest.java new file mode 100644 index 0000000..f61607e --- /dev/null +++ b/src/test/java/at/procon/eventhub/reference/TachographNationRegistryTest.java @@ -0,0 +1,51 @@ +package at.procon.eventhub.reference; + +import static org.assertj.core.api.Assertions.assertThat; + +import at.procon.eventhub.dto.DriverCardRefDto; +import at.procon.eventhub.dto.VehicleRegistrationRefDto; +import org.junit.jupiter.api.Test; + +class TachographNationRegistryTest { + + @Test + void resolvesKnownNumericNationCodeToAlphaAndNumeric() { + DriverCardRefDto driverCard = new DriverCardRefDto("13", "CARD-1"); + VehicleRegistrationRefDto registration = new VehicleRegistrationRefDto("13", "W-1"); + + assertThat(driverCard.nation()).isEqualTo("D"); + assertThat(driverCard.nationNumericCode()).isEqualTo(13); + assertThat(driverCard.stableKey()).isEqualTo("13:CARD-1"); + + assertThat(registration.nation()).isEqualTo("D"); + assertThat(registration.nationNumericCode()).isEqualTo(13); + assertThat(registration.stableKey()).isEqualTo("13:W-1"); + } + + @Test + void resolvesKnownAlphaNationCodeToNumeric() { + DriverCardRefDto driverCard = new DriverCardRefDto("A", "CARD-1"); + + assertThat(driverCard.nation()).isEqualTo("A"); + assertThat(driverCard.nationNumericCode()).isEqualTo(1); + assertThat(driverCard.stableKey()).isEqualTo("1:CARD-1"); + } + + @Test + void preservesUnknownNumericNationCodeAsSyntheticLabel() { + DriverCardRefDto driverCard = new DriverCardRefDto("209", "CARD-1"); + + assertThat(driverCard.nation()).isEqualTo("Unknown 209"); + assertThat(driverCard.nationNumericCode()).isEqualTo(209); + assertThat(driverCard.stableKey()).isEqualTo("209:CARD-1"); + } + + @Test + void keepsSyntheticTelematicsNationOutsideTachographMapping() { + VehicleRegistrationRefDto registration = new VehicleRegistrationRefDto("YELLOWFOX", "W-1"); + + assertThat(registration.nation()).isEqualTo("YELLOWFOX"); + assertThat(registration.nationNumericCode()).isNull(); + assertThat(registration.stableKey()).isEqualTo("YELLOWFOX:W-1"); + } +}