Add numeric tachograph nation resolution

This commit is contained in:
trifonovt 2026-05-20 14:31:14 +02:00
parent b35d428e80
commit fc0d6db99a
15 changed files with 724 additions and 38 deletions

View File

@ -1,14 +1,24 @@
package at.procon.eventhub.dto; package at.procon.eventhub.dto;
import at.procon.eventhub.reference.TachographNationRegistry;
/** /**
* Tachograph driver-card identifier. The card number is scoped by issuing nation. * Tachograph driver-card identifier. The card number is scoped by issuing nation.
*/ */
public record DriverCardRefDto( public record DriverCardRefDto(
String nation, String nation,
Integer nationNumericCode,
String number String number
) { ) {
public DriverCardRefDto(String nation, String number) {
this(nation, null, number);
}
public DriverCardRefDto { public DriverCardRefDto {
nation = normalize(nation); TachographNationRegistry.NationResolution nationResolution =
TachographNationRegistry.resolve(nation, nationNumericCode);
nation = normalize(nationResolution.legacyNation());
nationNumericCode = nationResolution.numericCode();
number = normalizeNullable(number); number = normalizeNullable(number);
} }
@ -17,11 +27,19 @@ public record DriverCardRefDto(
} }
public String stableKey() { 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) { 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) { private static String normalizeNullable(String value) {

View File

@ -1,15 +1,25 @@
package at.procon.eventhub.dto; package at.procon.eventhub.dto;
import at.procon.eventhub.reference.TachographNationRegistry;
/** /**
* Vehicle registration number reference. VRN/plate is scoped by nation and can * Vehicle registration number reference. VRN/plate is scoped by nation and can
* be resolved historically by occurredAt later. * be resolved historically by occurredAt later.
*/ */
public record VehicleRegistrationRefDto( public record VehicleRegistrationRefDto(
String nation, String nation,
Integer nationNumericCode,
String number String number
) { ) {
public VehicleRegistrationRefDto(String nation, String number) {
this(nation, null, number);
}
public VehicleRegistrationRefDto { public VehicleRegistrationRefDto {
nation = normalize(nation); TachographNationRegistry.NationResolution nationResolution =
TachographNationRegistry.resolve(nation, nationNumericCode);
nation = normalize(nationResolution.legacyNation());
nationNumericCode = nationResolution.numericCode();
number = normalizeNullable(number); number = normalizeNullable(number);
} }
@ -18,11 +28,19 @@ public record VehicleRegistrationRefDto(
} }
public String stableKey() { 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) { 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) { private static String normalizeNullable(String value) {

View File

@ -40,10 +40,11 @@ public class DriverIdentityRepository {
String sourceDriverEntityId = normalizeNullable(driverRef.sourceEntityId()); String sourceDriverEntityId = normalizeNullable(driverRef.sourceEntityId());
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();
String cardNumber = driverCard == null ? null : normalizeDriverCardNumber(cardNation, driverCard.number()); String cardNumber = driverCard == null ? null : normalizeDriverCardNumber(cardNation, driverCard.number());
UUID driverId = findBySourceDriverEntityId(normalizedTenantKey, eventSourceId, sourceDriverEntityId); UUID driverId = findBySourceDriverEntityId(normalizedTenantKey, eventSourceId, sourceDriverEntityId);
UUID driverCardId = resolveOrCreateDriverCardId(cardNation, cardNumber, driverId); UUID driverCardId = resolveOrCreateDriverCardId(cardNation, cardNationNumericCode, cardNumber, driverId);
if (driverId == null && driverCardId != null) { if (driverId == null && driverCardId != null) {
driverId = findDriverIdByCardId(driverCardId); driverId = findDriverIdByCardId(driverCardId);
@ -621,21 +622,24 @@ public class DriverIdentityRepository {
private UUID resolveOrCreateDriverCardId( private UUID resolveOrCreateDriverCardId(
String cardNation, String cardNation,
Integer cardNationNumericCode,
String cardNumber, String cardNumber,
UUID preferredDriverId UUID preferredDriverId
) { ) {
if (cardNation == null || cardNumber == null) { if ((cardNation == null && cardNationNumericCode == null) || cardNumber == null) {
return null; return null;
} }
UUID driverCardId = findDriverCardByCard(cardNation, cardNumber); UUID driverCardId = findDriverCardByCard(cardNation, cardNationNumericCode, cardNumber);
if (driverCardId == null) { if (driverCardId == null) {
Map<String, Object> payload = new LinkedHashMap<>(); Map<String, Object> payload = new LinkedHashMap<>();
put(payload, "source", "event"); put(payload, "source", "event");
put(payload, "card_nation", cardNation); put(payload, "card_nation", cardNation);
put(payload, "card_nation_numeric_code", cardNationNumericCode);
put(payload, "card_number", cardNumber); put(payload, "card_number", cardNumber);
return createDriverCard( return createDriverCard(
preferredDriverId, preferredDriverId,
cardNation, cardNation,
cardNationNumericCode,
cardNumber, cardNumber,
null, null,
payload payload
@ -683,10 +687,25 @@ public class DriverIdentityRepository {
); );
} }
private UUID findDriverCardByCard(String cardNation, String cardNumber) { private UUID findDriverCardByCard(String cardNation, Integer cardNationNumericCode, String cardNumber) {
if (cardNation == null || cardNumber == null) { if ((cardNation == null && cardNationNumericCode == null) || cardNumber == null) {
return 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( return jdbcTemplate.query(
""" """
select card.id select card.id
@ -782,6 +801,7 @@ public class DriverIdentityRepository {
private UUID createDriverCard( private UUID createDriverCard(
UUID driverId, UUID driverId,
String cardNation, String cardNation,
Integer cardNationNumericCode,
String cardNumber, String cardNumber,
OffsetDateTime sourceUpdatedAt, OffsetDateTime sourceUpdatedAt,
Map<String, Object> payload Map<String, Object> payload
@ -790,12 +810,13 @@ public class DriverIdentityRepository {
jdbcTemplate.update( jdbcTemplate.update(
""" """
insert into eventhub.driver_card( insert into eventhub.driver_card(
id, driver_id, nation, card_number, source_updated_at, payload, updated_at id, driver_id, nation, nation_numeric_code, card_number, source_updated_at, payload, updated_at
) values (?, ?, ?, ?, ?, ?::jsonb, now()) ) values (?, ?, ?, ?, ?, ?, ?::jsonb, now())
""", """,
driverCardId, driverCardId,
driverId, driverId,
cardNation, cardNation,
cardNationNumericCode,
cardNumber, cardNumber,
sourceUpdatedAt, sourceUpdatedAt,
toJson(payload) toJson(payload)

View File

@ -19,6 +19,7 @@ import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.dto.VehicleRegistrationRefDto; import at.procon.eventhub.dto.VehicleRegistrationRefDto;
import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest; import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest;
import at.procon.eventhub.reference.TachographNationRegistry;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -149,11 +150,13 @@ public class EventHubEventReadRepository {
detail.attributes, detail.attributes,
driver.source_driver_entity_id, driver.source_driver_entity_id,
driver_card.nation as driver_card_nation, 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, driver_card.card_number as driver_card_number,
vehicle.source_vehicle_entity_id, vehicle.source_vehicle_entity_id,
vehicle.vin, vehicle.vin,
registration.source_registration_entity_id, registration.source_registration_entity_id,
registration.nation as vehicle_registration_nation, registration.nation as vehicle_registration_nation,
registration.nation_numeric_code as vehicle_registration_nation_numeric_code,
registration.registration_number as vehicle_registration_number, registration.registration_number as vehicle_registration_number,
event.driver_id, event.driver_id,
event.vehicle_id, event.vehicle_id,
@ -209,8 +212,15 @@ public class EventHubEventReadRepository {
sql.append(" and driver_card.card_number = ?"); sql.append(" and driver_card.card_number = ?");
params.add(driverCardNumber); params.add(driverCardNumber);
if (driverCardNation != null) { if (driverCardNation != null) {
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 = ?"); sql.append(" and driver_card.nation = ?");
params.add(driverCardNation); params.add(driverCardNationResolution.legacyNation());
}
} }
} }
if (vehicleSourceEntityId != null) { if (vehicleSourceEntityId != null) {
@ -225,8 +235,15 @@ public class EventHubEventReadRepository {
sql.append(" and registration.registration_number = ?"); sql.append(" and registration.registration_number = ?");
params.add(registrationNumber); params.add(registrationNumber);
if (registrationNation != null) { if (registrationNation != null) {
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 = ?"); sql.append(" and registration.nation = ?");
params.add(registrationNation); params.add(registrationNationResolution.legacyNation());
}
} }
} }
@ -275,7 +292,11 @@ public class EventHubEventReadRepository {
String cardNumber = rs.getString("driver_card_number"); String cardNumber = rs.getString("driver_card_number");
DriverCardRefDto driverCard = cardNumber == null DriverCardRefDto driverCard = cardNumber == null
? 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); DriverRefDto driverRef = new DriverRefDto(sourceEntityId, driverCard);
return driverRef.hasAnyReference() ? driverRef : null; return driverRef.hasAnyReference() ? driverRef : null;
} }
@ -284,7 +305,11 @@ public class EventHubEventReadRepository {
VehicleRegistrationRefDto registration = null; VehicleRegistrationRefDto registration = null;
String registrationNumber = rs.getString("vehicle_registration_number"); String registrationNumber = rs.getString("vehicle_registration_number");
if (registrationNumber != null) { 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( VehicleRefDto vehicleRef = new VehicleRefDto(
firstNonBlank( firstNonBlank(
@ -406,6 +431,17 @@ public class EventHubEventReadRepository {
return Long.parseLong(value.toString()); 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) { private JsonNode json(String value) {
try { try {
return value == null || value.isBlank() return value == null || value.isBlank()

View File

@ -532,14 +532,18 @@ public class EventRepository {
String vin = normalizeNullable(vehicleRef.vin()); String vin = normalizeNullable(vehicleRef.vin());
String sourceRegistrationEntityId = normalizeNullable(vehicleRef.sourceRegistrationEntityId()); String sourceRegistrationEntityId = normalizeNullable(vehicleRef.sourceRegistrationEntityId());
VehicleRegistrationRefDto registration = vehicleRef.vehicleRegistration(); 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()); String registrationNumber = registration == null ? null : normalizeNullable(registration.number());
return String.join("|", return String.join("|",
sourceVehicleEntityId == null ? "" : sourceVehicleEntityId, sourceVehicleEntityId == null ? "" : sourceVehicleEntityId,
vin == null ? "" : vin, vin == null ? "" : vin,
sourceRegistrationEntityId == null ? "" : sourceRegistrationEntityId, sourceRegistrationEntityId == null ? "" : sourceRegistrationEntityId,
registrationNation == null ? "" : registrationNation, registrationNationKey == null ? "" : registrationNationKey,
registrationNumber == null ? "" : registrationNumber registrationNumber == null ? "" : registrationNumber
); );
} }

View File

@ -41,6 +41,7 @@ public class VehicleIdentityRepository {
String sourceRegistrationEntityId = normalizeNullable(vehicleRef.sourceRegistrationEntityId()); String sourceRegistrationEntityId = normalizeNullable(vehicleRef.sourceRegistrationEntityId());
VehicleRegistrationRefDto registration = vehicleRef.vehicleRegistration(); VehicleRegistrationRefDto registration = vehicleRef.vehicleRegistration();
String registrationNation = registration == null ? null : normalizeNullable(registration.nation()); String registrationNation = registration == null ? null : normalizeNullable(registration.nation());
Integer registrationNationNumericCode = registration == null ? null : registration.nationNumericCode();
String registrationNumber = registration == null ? null : normalizeNullable(registration.number()); String registrationNumber = registration == null ? null : normalizeNullable(registration.number());
String registrationNationForCreate = creationNation(registrationNation, registrationNumber); String registrationNationForCreate = creationNation(registrationNation, registrationNumber);
@ -49,14 +50,21 @@ public class VehicleIdentityRepository {
eventSourceId, eventSourceId,
sourceRegistrationEntityId, sourceRegistrationEntityId,
registrationNation, registrationNation,
registrationNationNumericCode,
registrationNumber registrationNumber
); );
if (registrationId == null && (sourceRegistrationEntityId != null || registrationNumber != null)) { if (registrationId == null && (sourceRegistrationEntityId != null || registrationNumber != null)) {
registrationId = createRegistration( registrationId = createRegistration(
registrationNationForCreate, registrationNationForCreate,
registrationNationNumericCode,
registrationNumber, registrationNumber,
null, 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) { if (registrationId != null && sourceRegistrationEntityId != null) {
@ -95,7 +103,7 @@ public class VehicleIdentityRepository {
); );
} }
touchVehicle(vehicleId, vin); touchVehicle(vehicleId, vin);
touchRegistration(registrationId, registrationNationForCreate, registrationNumber); touchRegistration(registrationId, registrationNationForCreate, registrationNationNumericCode, registrationNumber);
return new ResolvedVehicleReferenceResolution( return new ResolvedVehicleReferenceResolution(
new ResolvedVehicleReference(vehicleId, registrationId), new ResolvedVehicleReference(vehicleId, registrationId),
@ -617,6 +625,7 @@ public class VehicleIdentityRepository {
int eventSourceId, int eventSourceId,
String sourceRegistrationEntityId, String sourceRegistrationEntityId,
String nation, String nation,
Integer nationNumericCode,
String registrationNumber String registrationNumber
) { ) {
if (isYellowFoxSyntheticRegistrationNation(nation) && registrationNumber != null) { if (isYellowFoxSyntheticRegistrationNation(nation) && registrationNumber != null) {
@ -628,7 +637,7 @@ public class VehicleIdentityRepository {
} }
UUID registrationId = findRegistrationBySourceRegistrationEntityId(tenantKey, eventSourceId, sourceRegistrationEntityId); UUID registrationId = findRegistrationBySourceRegistrationEntityId(tenantKey, eventSourceId, sourceRegistrationEntityId);
if (registrationId == null) { if (registrationId == null) {
registrationId = findRegistrationByPlate(nation, registrationNumber); registrationId = findRegistrationByPlate(nation, nationNumericCode, registrationNumber);
} }
return registrationId; return registrationId;
} }
@ -696,10 +705,25 @@ public class VehicleIdentityRepository {
); );
} }
private UUID findRegistrationByPlate(String nation, String registrationNumber) { private UUID findRegistrationByPlate(String nation, Integer nationNumericCode, String registrationNumber) {
if (nation == null || registrationNumber == null) { if ((nation == null && nationNumericCode == null) || registrationNumber == null) {
return 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( return jdbcTemplate.query(
""" """
select r.id select r.id
@ -787,6 +811,7 @@ public class VehicleIdentityRepository {
private UUID createRegistration( private UUID createRegistration(
String nation, String nation,
Integer nationNumericCode,
String registrationNumber, String registrationNumber,
OffsetDateTime sourceUpdatedAt, OffsetDateTime sourceUpdatedAt,
Map<String, Object> payload Map<String, Object> payload
@ -798,11 +823,12 @@ public class VehicleIdentityRepository {
jdbcTemplate.update( jdbcTemplate.update(
""" """
insert into eventhub.vehicle_registration( insert into eventhub.vehicle_registration(
id, nation, registration_number, source_updated_at, payload, updated_at id, nation, nation_numeric_code, registration_number, source_updated_at, payload, updated_at
) values (?, ?, ?, ?, ?::jsonb, now()) ) values (?, ?, ?, ?, ?, ?::jsonb, now())
""", """,
registrationId, registrationId,
nation, nation,
nationNumericCode,
registrationNumber, registrationNumber,
sourceUpdatedAt, sourceUpdatedAt,
toJson(payload) toJson(payload)
@ -946,26 +972,30 @@ public class VehicleIdentityRepository {
); );
} }
private void touchRegistration(UUID registrationId, String nation, String registrationNumber) { private void touchRegistration(UUID registrationId, String nation, Integer nationNumericCode, String registrationNumber) {
if (registrationId == null || (nation == null && registrationNumber == null)) { if (registrationId == null || (nation == null && nationNumericCode == null && registrationNumber == null)) {
return; return;
} }
jdbcTemplate.update( jdbcTemplate.update(
""" """
update eventhub.vehicle_registration update eventhub.vehicle_registration
set nation = coalesce(cast(? as text), nation), 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), registration_number = coalesce(cast(? as text), registration_number),
updated_at = now() updated_at = now()
where id = ? where id = ?
and ( and (
(nation is null and cast(? as text) is not null) (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) or (registration_number is null and cast(? as text) is not null)
) )
""", """,
nation, nation,
nationNumericCode,
registrationNumber, registrationNumber,
registrationId, registrationId,
nation, nation,
nationNumericCode,
registrationNumber registrationNumber
); );
} }

View File

@ -15,6 +15,7 @@ import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend; import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; 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.dto.TachographImportRequest;
import at.procon.eventhub.tachograph.service.TachographExtractionDefinitionRegistry; import at.procon.eventhub.tachograph.service.TachographExtractionDefinitionRegistry;
import java.io.IOException; import java.io.IOException;
@ -199,11 +200,11 @@ public class TachographDbRuntimeEventLoader implements RuntimeDriverEventLoader,
params.put("lastSourcePackageImportedAt", null); params.put("lastSourcePackageImportedAt", null);
params.put("lastSourcePackageIdNumeric", null); params.put("lastSourcePackageIdNumeric", null);
params.put("driverSourceEntityId", request.driverSourceEntityId()); params.put("driverSourceEntityId", request.driverSourceEntityId());
params.put("driverCardNation", request.driverCardNation()); params.put("driverCardNation", alphaNation(request.driverCardNation()));
params.put("driverCardNumber", request.driverCardNumber()); params.put("driverCardNumber", request.driverCardNumber());
params.put("vehicleSourceEntityId", vehicleRef == null ? null : vehicleRef.sourceVehicleEntityId()); params.put("vehicleSourceEntityId", vehicleRef == null ? null : vehicleRef.sourceVehicleEntityId());
params.put("vehicleVin", vehicleRef == null ? null : vehicleRef.vin()); 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()); params.put("vehicleRegistrationNumber", vehicleRef == null ? null : vehicleRef.registrationNumber());
return params; return params;
} }
@ -251,4 +252,8 @@ public class TachographDbRuntimeEventLoader implements RuntimeDriverEventLoader,
throw new IllegalStateException("Failed to load tachograph runtime SQL " + sqlResource, ex); throw new IllegalStateException("Failed to load tachograph runtime SQL " + sqlResource, ex);
} }
} }
private String alphaNation(String rawNation) {
return TachographNationRegistry.alphaCode(rawNation, null);
}
} }

View File

@ -4,6 +4,7 @@ import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto; import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; 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.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.service.DriverTimelineEventBuilder; import at.procon.eventhub.tachographfilesession.service.DriverTimelineEventBuilder;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
@ -57,8 +58,7 @@ public class TachographFileSessionUnifiedVehicleEventSource implements UnifiedVe
return request.registrationNumber() != null return request.registrationNumber() != null
&& vehicleRef.vehicleRegistration() != null && vehicleRef.vehicleRegistration() != null
&& request.registrationNumber().equals(vehicleRef.vehicleRegistration().number()) && request.registrationNumber().equals(vehicleRef.vehicleRegistration().number())
&& (request.registrationNation() == null && matchesNation(request.registrationNation(), vehicleRef.vehicleRegistration().nation(), vehicleRef.vehicleRegistration().nationNumericCode());
|| request.registrationNation().equals(vehicleRef.vehicleRegistration().nation()));
} }
private boolean withinWindow( private boolean withinWindow(
@ -74,4 +74,18 @@ public class TachographFileSessionUnifiedVehicleEventSource implements UnifiedVe
} }
return occurredTo == null || !occurredAt.isAfter(occurredTo); 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());
}
} }

View File

@ -97,7 +97,11 @@ public class UnifiedRuntimeEventAssemblyService {
UnifiedDiscoveredVehicleRef candidate = new UnifiedDiscoveredVehicleRef( UnifiedDiscoveredVehicleRef candidate = new UnifiedDiscoveredVehicleRef(
vehicleRef.sourceVehicleEntityId(), vehicleRef.sourceVehicleEntityId(),
vehicleRef.vin(), 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() vehicleRef.vehicleRegistration() == null ? null : vehicleRef.vehicleRegistration().number()
); );
if (!candidate.hasAnyReference()) { if (!candidate.hasAnyReference()) {

View File

@ -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<Integer, NationRecord> BY_NUMERIC_CODE;
private static final Map<String, NationRecord> BY_ALPHA_CODE;
static {
Map<Integer, NationRecord> byNumeric = new LinkedHashMap<>();
Map<String, NationRecord> 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);
}
}
}

View File

@ -5,6 +5,7 @@ import at.procon.eventhub.dto.CardStatus;
import at.procon.eventhub.dto.DriverCardRefDto; import at.procon.eventhub.dto.DriverCardRefDto;
import at.procon.eventhub.dto.DrivingStatus; import at.procon.eventhub.dto.DrivingStatus;
import at.procon.eventhub.dto.EventDetailsDto; import at.procon.eventhub.dto.EventDetailsDto;
import at.procon.eventhub.reference.TachographNationRegistry;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -39,6 +40,7 @@ public class EventDetailsFactory {
put(attributes, "cardStatus", cardStatus); put(attributes, "cardStatus", cardStatus);
if (driverCard != null && driverCard.hasValue()) { if (driverCard != null && driverCard.hasValue()) {
put(attributes, "cardNation", driverCard.nation()); put(attributes, "cardNation", driverCard.nation());
put(attributes, "cardNationNumericCode", driverCard.nationNumericCode());
put(attributes, "cardNumber", driverCard.number()); put(attributes, "cardNumber", driverCard.number());
} }
return new EventDetailsDto("DRIVER_CARD", objectMapper.valueToTree(attributes)); return new EventDetailsDto("DRIVER_CARD", objectMapper.valueToTree(attributes));
@ -52,15 +54,21 @@ public class EventDetailsFactory {
public EventDetailsDto place(String country, String region) { public EventDetailsDto place(String country, String region) {
Map<String, Object> attributes = new LinkedHashMap<>(); Map<String, Object> 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); put(attributes, "region", region);
return new EventDetailsDto("PLACE", objectMapper.valueToTree(attributes)); return new EventDetailsDto("PLACE", objectMapper.valueToTree(attributes));
} }
public EventDetailsDto borderCrossing(String countryFrom, String countryTo) { public EventDetailsDto borderCrossing(String countryFrom, String countryTo) {
Map<String, Object> attributes = new LinkedHashMap<>(); Map<String, Object> attributes = new LinkedHashMap<>();
put(attributes, "countryFrom", countryFrom); TachographNationRegistry.NationResolution fromResolution = TachographNationRegistry.resolve(countryFrom, null);
put(attributes, "countryTo", countryTo); 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)); return new EventDetailsDto("BORDER_CROSSING", objectMapper.valueToTree(attributes));
} }

View File

@ -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) 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 ( create table if not exists eventhub.driver (
id uuid primary key, id uuid primary key,
first_names text, first_names text,
@ -160,6 +238,7 @@ create table if not exists eventhub.driver_card (
id uuid primary key, id uuid primary key,
driver_id uuid references eventhub.driver(id), driver_id uuid references eventhub.driver(id),
nation text not null, nation text not null,
nation_numeric_code integer references eventhub.nation(numeric_code),
card_number text not null, card_number text not null,
source_updated_at timestamptz, source_updated_at timestamptz,
payload jsonb not null default '{}'::jsonb, 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 ( create table if not exists eventhub.vehicle_registration (
id uuid primary key, id uuid primary key,
nation text not null, nation text not null,
nation_numeric_code integer references eventhub.nation(numeric_code),
registration_number text not null, registration_number text not null,
source_updated_at timestamptz, source_updated_at timestamptz,
payload jsonb not null default '{}'::jsonb, 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 create index if not exists idx_vehicle_registration_plate
on eventhub.vehicle_registration(nation, registration_number); 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 create index if not exists idx_vehicle_registration_assignment_registration_time
on eventhub.vehicle_registration_assignment(vehicle_registration_id, valid_from desc, valid_to); 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 create index if not exists idx_driver_card_key
on eventhub.driver_card(nation, card_number); 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 create index if not exists idx_driver_card_driver
on eventhub.driver_card(driver_id) on eventhub.driver_card(driver_id)
where driver_id is not null; 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 create unique index if not exists ux_vehicle_registration_plate
on eventhub.vehicle_registration(nation, registration_number); 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 create index if not exists idx_source_driver_identity_driver
on eventhub.source_driver_identity(driver_id); on eventhub.source_driver_identity(driver_id);

View File

@ -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
);

View File

@ -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
1 ID Name AlphaCode NumericCode DefaultLanguageCode ID_Certificate ID_FileLog
2 0 Unknown 0 0 0 NULL NULL NULL
3 1 Austria A 1 de-AT NULL NULL
4 2 Albania AL 2 sq-AL NULL NULL
5 3 Andorra AND 3 ca NULL NULL
6 4 Armenia ARM 4 hy-AM NULL NULL
7 5 Azerbaijan AZ 5 az NULL NULL
8 6 Belgium B 6 NULL NULL NULL
9 7 Bulgaria BG 7 bg-BG NULL NULL
10 8 Bosnia Herzegovina BIH 8 NULL NULL NULL
11 9 Belarus BY 9 be-BY NULL NULL
12 10 Switzerland CH 10 de-CH NULL NULL
13 11 Cyprus CY 11 NULL NULL NULL
14 12 Czech Republic CZ 12 cs-CZ NULL NULL
15 13 Germany D 13 de-DE NULL NULL
16 14 Denmark DK 14 da-DK NULL NULL
17 15 Spain E 15 es-ES NULL NULL
18 16 Estonia EST 16 et-EE NULL NULL
19 17 France F 17 fr-FR NULL NULL
20 18 Finland FIN 18 fi-FI NULL NULL
21 19 Liechtenstein FL 19 de-LI NULL NULL
22 20 Faroe Islands FR 20 fo-FO NULL NULL
23 21 United Kingdom UK 21 en-GB NULL NULL
24 22 Georgia GE 22 ka-GE NULL NULL
25 23 Greece GR 23 el-GR NULL NULL
26 24 Hungary H 24 hu-HU NULL NULL
27 25 Croatia HR 25 hr-HR NULL NULL
28 26 Italy I 26 it-IT NULL NULL
29 27 Ireland IRL 27 en-IE NULL NULL
30 28 Iceland IS 28 is-IS NULL NULL
31 29 Kazakhstan KZ 29 kk-KZ NULL NULL
32 30 Luxembourg L 30 de-LU NULL NULL
33 31 Lithuania LT 31 lt-LT NULL NULL
34 32 Latvia LV 32 lv-LV NULL NULL
35 33 Malta M 33 NULL NULL NULL
36 34 Monaco MC 34 fr-MC NULL NULL
37 35 Moldova MD 35 ro NULL NULL
38 36 North Macedonia MK 36 mk-MK NULL NULL
39 37 Norway N 37 no NULL NULL
40 38 Netherlands NL 38 nl-NL NULL NULL
41 39 Portugal P 39 pt-PT NULL NULL
42 40 Poland PL 40 pl-PL NULL NULL
43 41 Romania RO 41 ro-RO NULL NULL
44 42 San Marino RSM 42 it-IT NULL NULL
45 43 Russia RUS 43 ru-RU NULL NULL
46 44 Sweden S 44 sv-SE NULL NULL
47 45 Slovakia SK 45 sk-SK NULL NULL
48 46 Slovenia SLO 46 sl-SI NULL NULL
49 47 Turkmenistan TM 47 NULL NULL NULL
50 48 Turkey TR 48 tr-TR NULL NULL
51 49 Ukraine UA 49 uk-UA NULL NULL
52 50 Vatican City V 50 it-IT NULL NULL
53 51 Yugoslavia YU 51 NULL NULL NULL
54 52 Montenegro MNE 52 NULL NULL 763087
55 53 Serbia SRB 53 NULL NULL 272576
56 54 Uzbekistan UZ 54 NULL NULL 308001
57 55 Tajikistan TJ 55 NULL NULL NULL
58 253 European Community EC 253 NULL NULL NULL
59 254 Rest of Europe EUR 254 NULL NULL NULL
60 255 Rest of the world WLD 255 NULL NULL NULL

View File

@ -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");
}
}