Add tachograph place and position extractors

This commit is contained in:
trifonovt 2026-05-01 10:34:01 +02:00
parent 7ed0c73107
commit 9d9541bac9
21 changed files with 977 additions and 16 deletions

View File

@ -264,8 +264,8 @@ DRIVER_ACTIVITY / VEHICLE_UNIT -> VUActivity
DRIVER_ACTIVITY / DRIVER_CARD -> CardActivity
DRIVER_CARD / VEHICLE_UNIT -> IWCycle
DRIVER_CARD / DRIVER_CARD -> CardVehiclesUsed
POSITION / VEHICLE_UNIT -> VUPlaces, VULoadUnload, VUGnssAccumulatedDriving, VUBorderCrossing
POSITION / DRIVER_CARD -> CardPlaces, CardLoadUnload, CardGnssAccumulatedDriving, CardBorderCrossing
POSITION / VEHICLE_UNIT -> VUGnssAccumulatedDriving
POSITION / DRIVER_CARD -> CardGnssAccumulatedDriving
BORDER_CROSSING / VEHICLE_UNIT -> VUBorderCrossing
BORDER_CROSSING / DRIVER_CARD -> CardBorderCrossing
LOAD_UNLOAD / VEHICLE_UNIT -> VULoadUnload
@ -824,6 +824,10 @@ CARD_LOAD_UNLOAD -> LOAD_UNLOAD / DRIVER_CARD / CardLoadUnload
VU_LOAD_UNLOAD -> LOAD_UNLOAD / VEHICLE_UNIT / VULoadUnload
CARD_SPECIFIC_CONDITION -> SPECIFIC_CONDITION / DRIVER_CARD / CardSpecificCondition
VU_SPECIFIC_CONDITION -> SPECIFIC_CONDITION / VEHICLE_UNIT / VUSpecificCondition
CARD_POSITION -> POSITION / DRIVER_CARD / CardGnssAccumulatedDriving
VU_POSITION -> POSITION / VEHICLE_UNIT / VUGnssAccumulatedDriving
CARD_PLACE -> PLACE / DRIVER_CARD / CardPlaces
VU_PLACE -> PLACE / VEHICLE_UNIT / VUPlaces
```
SQL resources:
@ -831,12 +835,16 @@ SQL resources:
```text
src/main/resources/sql/tachograph/card-border-crossing.sql
src/main/resources/sql/tachograph/card-load-unload.sql
src/main/resources/sql/tachograph/card-place.sql
src/main/resources/sql/tachograph/card-position.sql
src/main/resources/sql/tachograph/card-specific-condition.sql
src/main/resources/sql/tachograph/card-vehicles-used.sql
src/main/resources/sql/tachograph/card-activity.sql
src/main/resources/sql/tachograph/iw-cycle.sql
src/main/resources/sql/tachograph/vu-border-crossing.sql
src/main/resources/sql/tachograph/vu-load-unload.sql
src/main/resources/sql/tachograph/vu-place.sql
src/main/resources/sql/tachograph/vu-position.sql
src/main/resources/sql/tachograph/vu-specific-condition.sql
src/main/resources/sql/tachograph/vu-activity.sql
```

View File

@ -7,8 +7,7 @@ public enum EventDomain {
POSITION,
BORDER_CROSSING,
LOAD_UNLOAD,
OUT_OF_SCOPE,
FERRY_TRAIN,
SPECIFIC_CONDITION,
SPEEDING,
PLACE,
VEHICLE_DATA,

View File

@ -20,8 +20,7 @@ public enum EventType {
OUT,
FERRY_TRAIN,
SPEEDING,
START_PLACE,
END_PLACE,
WORKING_DAY_PLACE_RECORDED,
VEHICLE_DATA,
TELEMATICS_DATA,
MANUAL_ENTRY,

View File

@ -50,6 +50,13 @@ public class EventDetailsFactory {
return new EventDetailsDto("POSITION", objectMapper.valueToTree(attributes));
}
public EventDetailsDto place(String country, String region) {
Map<String, Object> attributes = new LinkedHashMap<>();
put(attributes, "country", country);
put(attributes, "region", region);
return new EventDetailsDto("PLACE", objectMapper.valueToTree(attributes));
}
public EventDetailsDto borderCrossing(String countryFrom, String countryTo) {
Map<String, Object> attributes = new LinkedHashMap<>();
put(attributes, "countryFrom", countryFrom);

View File

@ -74,9 +74,18 @@ public class EventHubEventValidator {
if (event.eventDomain() == EventDomain.BORDER_CROSSING && !"BORDER_CROSSING".equals(detailType)) {
throw new IllegalArgumentException("BORDER_CROSSING events must use eventDetails.type=BORDER_CROSSING");
}
if (event.eventDomain() == EventDomain.POSITION && !"POSITION".equals(detailType)) {
throw new IllegalArgumentException("POSITION events must use eventDetails.type=POSITION");
}
if (event.eventDomain() == EventDomain.PLACE && !"PLACE".equals(detailType)) {
throw new IllegalArgumentException("PLACE events must use eventDetails.type=PLACE");
}
if (event.eventDomain() == EventDomain.LOAD_UNLOAD && !"LOAD_UNLOAD".equals(detailType)) {
throw new IllegalArgumentException("LOAD_UNLOAD events must use eventDetails.type=LOAD_UNLOAD");
}
if (event.eventDomain() == EventDomain.SPECIFIC_CONDITION && !"SPECIFIC_CONDITION".equals(detailType)) {
throw new IllegalArgumentException("SPECIFIC_CONDITION events must use eventDetails.type=SPECIFIC_CONDITION");
}
if (event.eventDomain() == EventDomain.SPEEDING && !"SPEEDING".equals(detailType)) {
throw new IllegalArgumentException("SPEEDING events must use eventDetails.type=SPEEDING");
}

View File

@ -0,0 +1,239 @@
package at.procon.eventhub.tachograph.service;
import at.procon.eventhub.dto.DriverCardRefDto;
import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventDomain;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.EventLifecycle;
import at.procon.eventhub.dto.EventType;
import at.procon.eventhub.dto.GeoPointDto;
import at.procon.eventhub.dto.SourcePackageRefDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
import at.procon.eventhub.importing.extraction.ExtractionContext;
import at.procon.eventhub.importing.extraction.ExtractionRowMapper;
import at.procon.eventhub.service.EventDetailsFactory;
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
abstract class AbstractTachographPlaceRowMapper implements ExtractionRowMapper<TachographImportRequest> {
private final EventDetailsFactory detailsFactory;
protected AbstractTachographPlaceRowMapper(EventDetailsFactory detailsFactory) {
this.detailsFactory = detailsFactory;
}
@Override
public EventHubEventDto map(ResultSet rs, int rowNum, ExtractionContext<TachographImportRequest> context) throws SQLException {
OffsetDateTime occurredAt = offsetDateTime(rs, "occurred_at");
SourcePackageRefDto sourcePackageRef = sourcePackageRef(rs);
DriverRefDto driverRef = driverRef(rs);
VehicleRefDto vehicleRef = vehicleRef(rs);
EventType eventType = eventType(rs);
EventLifecycle lifecycle = lifecycle(rs);
String externalSourceEventId = string(rs, "external_source_event_id");
if (externalSourceEventId == null) {
externalSourceEventId = defaultExternalSourceEventId(context, rowNum, occurredAt, sourcePackageRef, driverRef, vehicleRef, eventType);
}
return new EventHubEventDto(
UUID.randomUUID(),
externalSourceEventId,
driverRef,
vehicleRef,
occurredAt,
offsetDateTime(rs, "received_partner_at"),
OffsetDateTime.now(),
EventDomain.PLACE,
eventType,
lifecycle,
longValue(rs, "odometer_m"),
position(rs),
detailsFactory.place(string(rs, "country"), string(rs, "region")),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs)),
booleanValue(rs, "manual_entry"),
context.packageInfo()
);
}
protected Map<String, Object> sourceSpecificPayload(ResultSet rs) throws SQLException {
return Map.of();
}
private DriverRefDto driverRef(ResultSet rs) throws SQLException {
DriverCardRefDto driverCard = null;
String cardNumber = string(rs, "driver_card_number");
if (cardNumber != null) {
driverCard = new DriverCardRefDto(string(rs, "driver_card_nation"), cardNumber);
}
DriverRefDto driverRef = new DriverRefDto(string(rs, "driver_source_entity_id"), driverCard);
return driverRef.hasAnyReference() ? driverRef : null;
}
private VehicleRefDto vehicleRef(ResultSet rs) throws SQLException {
VehicleRegistrationRefDto registration = null;
String registrationNumber = string(rs, "vehicle_registration_number");
if (registrationNumber != null) {
registration = new VehicleRegistrationRefDto(string(rs, "vehicle_registration_nation"), registrationNumber);
}
VehicleRefDto vehicleRef = new VehicleRefDto(
string(rs, "vehicle_source_entity_id"),
string(rs, "vehicle_vin"),
string(rs, "vehicle_registration_source_entity_id"),
registration
);
return vehicleRef.hasAnyReference() ? vehicleRef : null;
}
private SourcePackageRefDto sourcePackageRef(ResultSet rs) throws SQLException {
return new SourcePackageRefDto(
string(rs, "source_package_kind"),
string(rs, "source_package_id"),
string(rs, "source_package_entity_id"),
offsetDateTime(rs, "source_package_period_from"),
offsetDateTime(rs, "source_package_period_to"),
offsetDateTime(rs, "source_package_imported_at")
);
}
private GeoPointDto position(ResultSet rs) throws SQLException {
BigDecimal latitude = decimal(rs, "latitude");
BigDecimal longitude = decimal(rs, "longitude");
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
}
private Map<String, Object> payload(ResultSet rs) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
raw.putAll(sourceSpecificPayload(rs));
return Map.of("raw", raw);
}
private String defaultExternalSourceEventId(
ExtractionContext<TachographImportRequest> context,
int rowNum,
OffsetDateTime occurredAt,
SourcePackageRefDto sourcePackageRef,
DriverRefDto driverRef,
VehicleRefDto vehicleRef,
EventType eventType
) {
String sourcePackageId = sourcePackageRef == null || sourcePackageRef.sourcePackageId() == null
? "NO_SOURCE_PACKAGE"
: sourcePackageRef.sourcePackageId();
String subject = driverRef != null && driverRef.hasAnyReference()
? driverRef.stableKey()
: vehicleRef == null ? "NO_SUBJECT" : vehicleRef.stableKey();
return "TACHOGRAPH:" + context.planItem().extractionCode()
+ ":" + sourcePackageId
+ ":" + eventType
+ ":" + occurredAt
+ ":" + subject
+ ":ROW-" + rowNum;
}
protected String string(ResultSet rs, String column) throws SQLException {
String value = rs.getString(column);
return value == null || value.isBlank() ? null : value.trim();
}
protected OffsetDateTime offsetDateTime(ResultSet rs, String column) throws SQLException {
Object value = rs.getObject(column);
if (value == null) {
return null;
}
if (value instanceof OffsetDateTime offsetDateTime) {
return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
}
if (value instanceof Timestamp timestamp) {
return timestamp.toLocalDateTime().atOffset(ZoneOffset.UTC);
}
if (value instanceof LocalDateTime localDateTime) {
return localDateTime.atOffset(ZoneOffset.UTC);
}
String text = value.toString();
try {
return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC);
} catch (RuntimeException ignored) {
return LocalDateTime.parse(text).atOffset(ZoneOffset.UTC);
}
}
private Long longValue(ResultSet rs, String column) throws SQLException {
Object value = rs.getObject(column);
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.longValue();
}
return Long.parseLong(value.toString());
}
private BigDecimal decimal(ResultSet rs, String column) throws SQLException {
Object value = rs.getObject(column);
if (value == null) {
return null;
}
if (value instanceof BigDecimal bigDecimal) {
return bigDecimal;
}
if (value instanceof Number number) {
return BigDecimal.valueOf(number.doubleValue());
}
return new BigDecimal(value.toString());
}
private Boolean booleanValue(ResultSet rs, String column) throws SQLException {
Object value = rs.getObject(column);
if (value == null) {
return false;
}
if (value instanceof Boolean bool) {
return bool;
}
if (value instanceof Number number) {
return number.intValue() != 0;
}
return Boolean.parseBoolean(value.toString());
}
private EventType eventType(ResultSet rs) throws SQLException {
return parseEnum(EventType.class, string(rs, "event_type"), EventType.POSITION_RECORDED);
}
private EventLifecycle lifecycle(ResultSet rs) throws SQLException {
return parseEnum(EventLifecycle.class, string(rs, "lifecycle"), EventLifecycle.SNAPSHOT);
}
private <T extends Enum<T>> T parseEnum(Class<T> type, String value, T fallback) {
if (value == null) {
return fallback;
}
String normalized = value.trim().toUpperCase(Locale.ROOT).replace('-', '_').replace(' ', '_');
try {
return Enum.valueOf(type, normalized);
} catch (IllegalArgumentException ignored) {
return fallback;
}
}
protected void put(Map<String, Object> target, String key, Object value) {
if (value != null) {
target.put(key, value);
}
}
}

View File

@ -0,0 +1,201 @@
package at.procon.eventhub.tachograph.service;
import at.procon.eventhub.dto.DriverCardRefDto;
import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventDomain;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.EventLifecycle;
import at.procon.eventhub.dto.EventType;
import at.procon.eventhub.dto.GeoPointDto;
import at.procon.eventhub.dto.SourcePackageRefDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
import at.procon.eventhub.importing.extraction.ExtractionContext;
import at.procon.eventhub.importing.extraction.ExtractionRowMapper;
import at.procon.eventhub.service.EventDetailsFactory;
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
abstract class AbstractTachographPositionRowMapper implements ExtractionRowMapper<TachographImportRequest> {
private final EventDetailsFactory detailsFactory;
protected AbstractTachographPositionRowMapper(EventDetailsFactory detailsFactory) {
this.detailsFactory = detailsFactory;
}
@Override
public EventHubEventDto map(ResultSet rs, int rowNum, ExtractionContext<TachographImportRequest> context) throws SQLException {
OffsetDateTime occurredAt = offsetDateTime(rs, "occurred_at");
SourcePackageRefDto sourcePackageRef = sourcePackageRef(rs);
DriverRefDto driverRef = driverRef(rs);
VehicleRefDto vehicleRef = vehicleRef(rs);
String externalSourceEventId = string(rs, "external_source_event_id");
if (externalSourceEventId == null) {
externalSourceEventId = defaultExternalSourceEventId(context, rowNum, occurredAt, sourcePackageRef, driverRef, vehicleRef);
}
return new EventHubEventDto(
UUID.randomUUID(),
externalSourceEventId,
driverRef,
vehicleRef,
occurredAt,
offsetDateTime(rs, "received_partner_at"),
OffsetDateTime.now(),
EventDomain.POSITION,
EventType.POSITION_RECORDED,
EventLifecycle.SNAPSHOT,
longValue(rs, "odometer_m"),
position(rs),
detailsFactory.position("GNSS_ACCUMULATED_DRIVING"),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs)),
false,
context.packageInfo()
);
}
protected Map<String, Object> sourceSpecificPayload(ResultSet rs) throws SQLException {
return Map.of();
}
private DriverRefDto driverRef(ResultSet rs) throws SQLException {
DriverCardRefDto driverCard = null;
String cardNumber = string(rs, "driver_card_number");
if (cardNumber != null) {
driverCard = new DriverCardRefDto(string(rs, "driver_card_nation"), cardNumber);
}
DriverRefDto driverRef = new DriverRefDto(string(rs, "driver_source_entity_id"), driverCard);
return driverRef.hasAnyReference() ? driverRef : null;
}
private VehicleRefDto vehicleRef(ResultSet rs) throws SQLException {
VehicleRegistrationRefDto registration = null;
String registrationNumber = string(rs, "vehicle_registration_number");
if (registrationNumber != null) {
registration = new VehicleRegistrationRefDto(string(rs, "vehicle_registration_nation"), registrationNumber);
}
VehicleRefDto vehicleRef = new VehicleRefDto(
string(rs, "vehicle_source_entity_id"),
string(rs, "vehicle_vin"),
string(rs, "vehicle_registration_source_entity_id"),
registration
);
return vehicleRef.hasAnyReference() ? vehicleRef : null;
}
private SourcePackageRefDto sourcePackageRef(ResultSet rs) throws SQLException {
return new SourcePackageRefDto(
string(rs, "source_package_kind"),
string(rs, "source_package_id"),
string(rs, "source_package_entity_id"),
offsetDateTime(rs, "source_package_period_from"),
offsetDateTime(rs, "source_package_period_to"),
offsetDateTime(rs, "source_package_imported_at")
);
}
private GeoPointDto position(ResultSet rs) throws SQLException {
BigDecimal latitude = decimal(rs, "latitude");
BigDecimal longitude = decimal(rs, "longitude");
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
}
private Map<String, Object> payload(ResultSet rs) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
raw.putAll(sourceSpecificPayload(rs));
return Map.of("raw", raw);
}
private String defaultExternalSourceEventId(
ExtractionContext<TachographImportRequest> context,
int rowNum,
OffsetDateTime occurredAt,
SourcePackageRefDto sourcePackageRef,
DriverRefDto driverRef,
VehicleRefDto vehicleRef
) {
String sourcePackageId = sourcePackageRef == null || sourcePackageRef.sourcePackageId() == null
? "NO_SOURCE_PACKAGE"
: sourcePackageRef.sourcePackageId();
String subject = driverRef != null && driverRef.hasAnyReference()
? driverRef.stableKey()
: vehicleRef == null ? "NO_SUBJECT" : vehicleRef.stableKey();
return "TACHOGRAPH:" + context.planItem().extractionCode()
+ ":" + sourcePackageId
+ ":POSITION_RECORDED"
+ ":" + occurredAt
+ ":" + subject
+ ":ROW-" + rowNum;
}
protected String string(ResultSet rs, String column) throws SQLException {
String value = rs.getString(column);
return value == null || value.isBlank() ? null : value.trim();
}
protected OffsetDateTime offsetDateTime(ResultSet rs, String column) throws SQLException {
Object value = rs.getObject(column);
if (value == null) {
return null;
}
if (value instanceof OffsetDateTime offsetDateTime) {
return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
}
if (value instanceof Timestamp timestamp) {
return timestamp.toLocalDateTime().atOffset(ZoneOffset.UTC);
}
if (value instanceof LocalDateTime localDateTime) {
return localDateTime.atOffset(ZoneOffset.UTC);
}
String text = value.toString();
try {
return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC);
} catch (RuntimeException ignored) {
return LocalDateTime.parse(text).atOffset(ZoneOffset.UTC);
}
}
private Long longValue(ResultSet rs, String column) throws SQLException {
Object value = rs.getObject(column);
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.longValue();
}
return Long.parseLong(value.toString());
}
private BigDecimal decimal(ResultSet rs, String column) throws SQLException {
Object value = rs.getObject(column);
if (value == null) {
return null;
}
if (value instanceof BigDecimal bigDecimal) {
return bigDecimal;
}
if (value instanceof Number number) {
return BigDecimal.valueOf(number.doubleValue());
}
return new BigDecimal(value.toString());
}
protected void put(Map<String, Object> target, String key, Object value) {
if (value != null) {
target.put(key, value);
}
}
}

View File

@ -168,7 +168,7 @@ abstract class AbstractTachographSpecificConditionRowMapper implements Extractio
}
private EventDomain eventDomain(ResultSet rs) throws SQLException {
return parseEnum(EventDomain.class, string(rs, "event_domain"), EventDomain.OUT_OF_SCOPE);
return parseEnum(EventDomain.class, string(rs, "event_domain"), EventDomain.SPECIFIC_CONDITION);
}
private EventType eventType(ResultSet rs) throws SQLException {

View File

@ -0,0 +1,12 @@
package at.procon.eventhub.tachograph.service;
import at.procon.eventhub.service.EventDetailsFactory;
import org.springframework.stereotype.Component;
@Component
public class CardPlaceRowMapper extends AbstractTachographPlaceRowMapper {
public CardPlaceRowMapper(EventDetailsFactory detailsFactory) {
super(detailsFactory);
}
}

View File

@ -0,0 +1,12 @@
package at.procon.eventhub.tachograph.service;
import at.procon.eventhub.service.EventDetailsFactory;
import org.springframework.stereotype.Component;
@Component
public class CardPositionRowMapper extends AbstractTachographPositionRowMapper {
public CardPositionRowMapper(EventDetailsFactory detailsFactory) {
super(detailsFactory);
}
}

View File

@ -20,7 +20,11 @@ public class TachographExtractionDefinitionRegistry extends ExtractionDefinition
VuLoadUnloadRowMapper vuLoadUnloadRowMapper,
CardLoadUnloadRowMapper cardLoadUnloadRowMapper,
VuSpecificConditionRowMapper vuSpecificConditionRowMapper,
CardSpecificConditionRowMapper cardSpecificConditionRowMapper
CardSpecificConditionRowMapper cardSpecificConditionRowMapper,
VuPositionRowMapper vuPositionRowMapper,
CardPositionRowMapper cardPositionRowMapper,
VuPlaceRowMapper vuPlaceRowMapper,
CardPlaceRowMapper cardPlaceRowMapper
) {
super(List.of(
new ExtractionDefinition<>(
@ -102,6 +106,38 @@ public class TachographExtractionDefinitionRegistry extends ExtractionDefinition
"DRIVER",
"classpath:sql/tachograph/card-specific-condition.sql",
cardSpecificConditionRowMapper
),
new ExtractionDefinition<>(
"VU_POSITION",
EventFamily.POSITION,
"VEHICLE_UNIT",
"VEHICLE",
"classpath:sql/tachograph/vu-position.sql",
vuPositionRowMapper
),
new ExtractionDefinition<>(
"CARD_POSITION",
EventFamily.POSITION,
"DRIVER_CARD",
"DRIVER",
"classpath:sql/tachograph/card-position.sql",
cardPositionRowMapper
),
new ExtractionDefinition<>(
"VU_PLACE",
EventFamily.PLACE,
"VEHICLE_UNIT",
"VEHICLE",
"classpath:sql/tachograph/vu-place.sql",
vuPlaceRowMapper
),
new ExtractionDefinition<>(
"CARD_PLACE",
EventFamily.PLACE,
"DRIVER_CARD",
"DRIVER",
"classpath:sql/tachograph/card-place.sql",
cardPlaceRowMapper
)
));
}

View File

@ -60,8 +60,8 @@ public class TachographImportPlanService {
item(family, "DRIVER_CARD", "CARD_VEHICLES_USED", List.of("CardVehiclesUsed"), "DRIVER", "Card insert/withdraw/use events from card vehicle usage", strategy)
);
case POSITION -> List.of(
item(family, "VEHICLE_UNIT", "VU_POSITION", List.of("VUPlaces", "VULoadUnload", "VUGnssAccumulatedDriving", "VUBorderCrossing"), "VEHICLE", "Position points from VU tachograph sources", strategy),
item(family, "DRIVER_CARD", "CARD_POSITION", List.of("CardPlaces", "CardLoadUnload", "CardGnssAccumulatedDriving", "CardBorderCrossing"), "DRIVER", "Position points from driver-card tachograph sources", strategy)
item(family, "VEHICLE_UNIT", "VU_POSITION", List.of("VUGnssAccumulatedDriving"), "VEHICLE", "Periodic GNSS position points from VU", strategy),
item(family, "DRIVER_CARD", "CARD_POSITION", List.of("CardGnssAccumulatedDriving"), "DRIVER", "Periodic GNSS position points from driver card", strategy)
);
case BORDER_CROSSING -> List.of(
item(family, "VEHICLE_UNIT", "VU_BORDER_CROSSING", List.of("VUBorderCrossing"), "VEHICLE", "Border crossing events from VU", strategy),

View File

@ -0,0 +1,12 @@
package at.procon.eventhub.tachograph.service;
import at.procon.eventhub.service.EventDetailsFactory;
import org.springframework.stereotype.Component;
@Component
public class VuPlaceRowMapper extends AbstractTachographPlaceRowMapper {
public VuPlaceRowMapper(EventDetailsFactory detailsFactory) {
super(detailsFactory);
}
}

View File

@ -0,0 +1,12 @@
package at.procon.eventhub.tachograph.service;
import at.procon.eventhub.service.EventDetailsFactory;
import org.springframework.stereotype.Component;
@Component
public class VuPositionRowMapper extends AbstractTachographPositionRowMapper {
public VuPositionRowMapper(EventDetailsFactory detailsFactory) {
super(detailsFactory);
}
}

View File

@ -0,0 +1,100 @@
/*
* CardPlaces PLACE extraction for the bytebar tachograph schema.
*/
with OrgTree as (
select org.I_90021_OID
from dbo.GetOrganisationTree(null, :organisationId, 0, null) org
where :organisationId is not null
)
,
Base as (
select
place.ID,
place.EntryTime as occurred_at,
cast(place.EntryType as int) as entry_type,
place.Odo,
place.ID_FileLog,
c.ID as card_id,
c.ID_Driver as driver_id,
cn.AlphaCode as driver_card_nation,
c.CardNumber as driver_card_number,
placeNation.AlphaCode as country,
coalesce(region.AlphaCode, region.Name) as region,
gnss.Latitude as latitude,
gnss.Longitude as longitude,
v.ID as vehicle_registration_id,
v.ID_VehicleIdentification as vehicle_identification_id,
vi.VIN as vehicle_vin,
vehicleNation.AlphaCode as vehicle_registration_nation,
v.VRN as vehicle_registration_number,
coalesce(fl.DownloadDate, fl.OriginalDownloadDate, fl.TStamp, fl.CreationDate) as received_partner_at,
coalesce(fl.ID, place.ID_FileLog, place.ID) as source_package_id_raw,
coalesce(fl.ID_Card, place.ID_Card) as source_package_entity_id_raw,
coalesce(fl.DownloadFrom, place.EntryTime) as source_package_period_from,
coalesce(fl.DownloadTo, place.EntryTime) as source_package_period_to,
coalesce(fl.CreationDate, fl.TStamp) as source_package_imported_at
from dbo.CardPlaces place
join dbo.Card c on c.ID = place.ID_Card
left join dbo.Nation cn on cn.ID = c.ID_Nation
left join dbo.Nation placeNation on placeNation.ID = place.ID_Nation
left join dbo.Region region on region.ID = place.ID_Region
left join dbo.GnssPlace gnss on gnss.ID = place.ID_GnssPlace
left join dbo.FileLog fl on fl.ID = place.ID_FileLog
outer apply (
select top 1 used.ID_Vehicle
from dbo.CardVehiclesUsed used
where used.ID_Card = place.ID_Card
and (used.FirstUse is null or used.FirstUse <= place.EntryTime)
and (used.LastUse is null or used.LastUse >= place.EntryTime)
order by
used.FirstUse desc,
used.ID desc
) cvu
left join dbo.Vehicle v on v.ID = cvu.ID_Vehicle
left join dbo.VehicleIdentification vi on vi.ID = v.ID_VehicleIdentification
left join dbo.Nation vehicleNation on vehicleNation.ID = v.ID_Nation
where (:occurredFrom is null or place.EntryTime >= :occurredFrom)
and (:occurredTo is null or place.EntryTime < :occurredTo)
and (
:organisationId is null
or exists (
select 1
from dbo.Driver_I_90021 rel
join OrgTree on OrgTree.I_90021_OID = rel.ID_I_90021
where rel.ID_Driver = c.ID_Driver
and rel.GILT_BIS is null
)
)
)
select
cast(base.ID as varchar(128)) as source_row_id,
concat('TACHOGRAPH:CARD_PLACE:', base.ID) as external_source_event_id,
base.occurred_at,
base.received_partner_at,
cast(base.Odo as bigint) * 1000 as odometer_m,
base.latitude,
base.longitude,
base.country,
base.region,
'WORKING_DAY_PLACE_RECORDED' as event_type,
case when base.entry_type in (0, 2, 4) then 'START' else 'END' end as lifecycle,
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
base.driver_card_nation,
base.driver_card_number,
cast(base.vehicle_identification_id as varchar(128)) as vehicle_source_entity_id,
base.vehicle_vin,
cast(base.vehicle_registration_id as varchar(128)) as vehicle_registration_source_entity_id,
base.vehicle_registration_nation,
base.vehicle_registration_number,
'DRIVER_CARD' as source_package_kind,
cast(base.source_package_id_raw as varchar(128)) as source_package_id,
cast(base.source_package_entity_id_raw as varchar(128)) as source_package_entity_id,
base.source_package_period_from,
base.source_package_period_to,
base.source_package_imported_at
from Base base

View File

@ -0,0 +1,90 @@
/*
* CardGnssAccumulatedDriving POSITION extraction for the bytebar tachograph schema.
*/
with OrgTree as (
select org.I_90021_OID
from dbo.GetOrganisationTree(null, :organisationId, 0, null) org
where :organisationId is not null
)
,
Base as (
select
pos.ID,
pos.Timestamp as occurred_at,
pos.Odo,
pos.ID_FileLog,
c.ID as card_id,
c.ID_Driver as driver_id,
cn.AlphaCode as driver_card_nation,
c.CardNumber as driver_card_number,
gnss.Latitude as latitude,
gnss.Longitude as longitude,
v.ID as vehicle_registration_id,
v.ID_VehicleIdentification as vehicle_identification_id,
vi.VIN as vehicle_vin,
vehicleNation.AlphaCode as vehicle_registration_nation,
v.VRN as vehicle_registration_number,
coalesce(fl.DownloadDate, fl.OriginalDownloadDate, fl.TStamp, fl.CreationDate) as received_partner_at,
coalesce(fl.ID, pos.ID_FileLog, pos.ID) as source_package_id_raw,
coalesce(fl.ID_Card, pos.ID_Card) as source_package_entity_id_raw,
coalesce(fl.DownloadFrom, pos.Timestamp) as source_package_period_from,
coalesce(fl.DownloadTo, pos.Timestamp) as source_package_period_to,
coalesce(fl.CreationDate, fl.TStamp) as source_package_imported_at
from dbo.CardGnssAccumulatedDriving pos
join dbo.Card c on c.ID = pos.ID_Card
left join dbo.Nation cn on cn.ID = c.ID_Nation
join dbo.GnssPlace gnss on gnss.ID = pos.ID_GnssPlace
left join dbo.FileLog fl on fl.ID = pos.ID_FileLog
outer apply (
select top 1 used.ID_Vehicle
from dbo.CardVehiclesUsed used
where used.ID_Card = pos.ID_Card
and (used.FirstUse is null or used.FirstUse <= pos.Timestamp)
and (used.LastUse is null or used.LastUse >= pos.Timestamp)
order by
used.FirstUse desc,
used.ID desc
) cvu
left join dbo.Vehicle v on v.ID = cvu.ID_Vehicle
left join dbo.VehicleIdentification vi on vi.ID = v.ID_VehicleIdentification
left join dbo.Nation vehicleNation on vehicleNation.ID = v.ID_Nation
where (:occurredFrom is null or pos.Timestamp >= :occurredFrom)
and (:occurredTo is null or pos.Timestamp < :occurredTo)
and (
:organisationId is null
or exists (
select 1
from dbo.Driver_I_90021 rel
join OrgTree on OrgTree.I_90021_OID = rel.ID_I_90021
where rel.ID_Driver = c.ID_Driver
and rel.GILT_BIS is null
)
)
)
select
cast(base.ID as varchar(128)) as source_row_id,
concat('TACHOGRAPH:CARD_POSITION:', base.ID) as external_source_event_id,
base.occurred_at,
base.received_partner_at,
cast(base.Odo as bigint) * 1000 as odometer_m,
base.latitude,
base.longitude,
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
base.driver_card_nation,
base.driver_card_number,
cast(base.vehicle_identification_id as varchar(128)) as vehicle_source_entity_id,
base.vehicle_vin,
cast(base.vehicle_registration_id as varchar(128)) as vehicle_registration_source_entity_id,
base.vehicle_registration_nation,
base.vehicle_registration_number,
'DRIVER_CARD' as source_package_kind,
cast(base.source_package_id_raw as varchar(128)) as source_package_id,
cast(base.source_package_entity_id_raw as varchar(128)) as source_package_entity_id,
base.source_package_period_from,
base.source_package_period_to,
base.source_package_imported_at
from Base base

View File

@ -65,7 +65,7 @@ select
base.occurred_at,
base.received_partner_at,
case when base.condition_code in (3, 4) then 'FERRY_TRAIN' else 'OUT_OF_SCOPE' end as event_domain,
'SPECIFIC_CONDITION' as event_domain,
case when base.condition_code in (3, 4) then 'FERRY_TRAIN' else 'OUT' end as event_type,
case when base.condition_code in (1, 3) then 'BEGIN' else 'END' end as lifecycle,

View File

@ -0,0 +1,101 @@
/*
* VUPlaces PLACE extraction for the bytebar tachograph schema.
*/
with OrgTree as (
select org.I_90021_OID
from dbo.GetOrganisationTree(null, :organisationId, 0, null) org
where :organisationId is not null
)
,
Base as (
select
place.ID,
place.EntryTime as occurred_at,
place.EntryType as entry_type,
place.Odo,
place.ID_FileLog,
c.ID as card_id,
c.ID_Driver as driver_id,
cn.AlphaCode as driver_card_nation,
c.CardNumber as driver_card_number,
placeNation.AlphaCode as country,
coalesce(region.AlphaCode, region.Name) as region,
gnss.Latitude as latitude,
gnss.Longitude as longitude,
vui.ID_FileLog as vui_filelog_id,
vui.ID_VehicleIdentification,
vi.ID as vehicle_identification_id,
vi.VIN as vehicle_vin,
coalesce(fl.DownloadDate, fl.OriginalDownloadDate, fl.TStamp, fl.CreationDate) as received_partner_at,
coalesce(fl.ID, place.ID_FileLog, vui.ID_FileLog, place.ID) as source_package_id_raw,
coalesce(fl.ID_VehicleIdentification, vui.ID_VehicleIdentification) as source_package_entity_id_raw,
coalesce(fl.DownloadFrom, place.EntryTime) as source_package_period_from,
coalesce(fl.DownloadTo, place.EntryTime) as source_package_period_to,
coalesce(fl.CreationDate, fl.TStamp) as source_package_imported_at
from dbo.VUPlaces place
join dbo.VUInstallation vui on vui.ID = place.ID_VUInstallation
join dbo.VehicleIdentification vi on vi.ID = vui.ID_VehicleIdentification
left join dbo.Card c on c.ID = place.ID_Card
left join dbo.Nation cn on cn.ID = c.ID_Nation
left join dbo.Nation placeNation on placeNation.ID = place.ID_Nation
left join dbo.Region region on region.ID = place.ID_Region
left join dbo.GnssPlace gnss on gnss.ID = place.ID_GnssPlace
left join dbo.FileLog fl on fl.ID = coalesce(place.ID_FileLog, vui.ID_FileLog)
where (:occurredFrom is null or place.EntryTime >= :occurredFrom)
and (:occurredTo is null or place.EntryTime < :occurredTo)
)
select
cast(base.ID as varchar(128)) as source_row_id,
concat('TACHOGRAPH:VU_PLACE:', base.ID) as external_source_event_id,
base.occurred_at,
base.received_partner_at,
cast(base.Odo as bigint) * 1000 as odometer_m,
base.latitude,
base.longitude,
base.country,
base.region,
'WORKING_DAY_PLACE_RECORDED' as event_type,
case when base.entry_type in (0, 2, 4) then 'START' else 'END' end as lifecycle,
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
base.driver_card_nation,
base.driver_card_number,
cast(base.vehicle_identification_id as varchar(128)) as vehicle_source_entity_id,
base.vehicle_vin,
cast(v.ID as varchar(128)) as vehicle_registration_source_entity_id,
vn.AlphaCode as vehicle_registration_nation,
v.VRN as vehicle_registration_number,
'VEHICLE_UNIT' as source_package_kind,
cast(base.source_package_id_raw as varchar(128)) as source_package_id,
cast(base.source_package_entity_id_raw as varchar(128)) as source_package_entity_id,
base.source_package_period_from,
base.source_package_period_to,
base.source_package_imported_at
from Base base
outer apply (
select top 1 vehicle.ID,
vehicle.VRN,
vehicle.ID_Nation
from dbo.Vehicle vehicle
where vehicle.ID_VehicleIdentification = base.vehicle_identification_id
and (vehicle.ValidFrom is null or vehicle.ValidFrom <= base.occurred_at)
and (vehicle.ValidTo is null or vehicle.ValidTo > base.occurred_at)
order by
vehicle.ValidFrom desc,
vehicle.ID desc
) v
left join dbo.Nation vn on vn.ID = v.ID_Nation
where (
:organisationId is null
or exists (
select 1
from dbo.Vehicle_I_90021 rel
join OrgTree on OrgTree.I_90021_OID = rel.ID_I_90021
where rel.ID_Vehicle = v.ID
and rel.GILT_BIS is null
)
)

View File

@ -0,0 +1,94 @@
/*
* VUGnssAccumulatedDriving POSITION extraction for the bytebar tachograph schema.
*/
with OrgTree as (
select org.I_90021_OID
from dbo.GetOrganisationTree(null, :organisationId, 0, null) org
where :organisationId is not null
)
,
Base as (
select
pos.ID,
pos.Timestamp as occurred_at,
pos.Odo,
pos.ID_FileLog,
pos.ID_VUInstallation,
coalesce(driverCard.ID, coDriverCard.ID) as card_id,
coalesce(driverCard.ID_Driver, coDriverCard.ID_Driver) as driver_id,
coalesce(driverNation.AlphaCode, coDriverNation.AlphaCode) as driver_card_nation,
coalesce(driverCard.CardNumber, coDriverCard.CardNumber) as driver_card_number,
gnss.Latitude as latitude,
gnss.Longitude as longitude,
vui.ID_FileLog as vui_filelog_id,
vui.ID_VehicleIdentification,
vi.ID as vehicle_identification_id,
vi.VIN as vehicle_vin,
coalesce(fl.DownloadDate, fl.OriginalDownloadDate, fl.TStamp, fl.CreationDate) as received_partner_at,
coalesce(fl.ID, pos.ID_FileLog, vui.ID_FileLog, pos.ID) as source_package_id_raw,
coalesce(fl.ID_VehicleIdentification, vui.ID_VehicleIdentification) as source_package_entity_id_raw,
coalesce(fl.DownloadFrom, pos.Timestamp) as source_package_period_from,
coalesce(fl.DownloadTo, pos.Timestamp) as source_package_period_to,
coalesce(fl.CreationDate, fl.TStamp) as source_package_imported_at
from dbo.VUGnssAccumulatedDriving pos
join dbo.VUInstallation vui on vui.ID = pos.ID_VUInstallation
join dbo.VehicleIdentification vi on vi.ID = vui.ID_VehicleIdentification
left join dbo.Card driverCard on driverCard.ID = pos.ID_DriverCard
left join dbo.Nation driverNation on driverNation.ID = driverCard.ID_Nation
left join dbo.Card coDriverCard on coDriverCard.ID = pos.ID_CoDriverCard
left join dbo.Nation coDriverNation on coDriverNation.ID = coDriverCard.ID_Nation
join dbo.GnssPlace gnss on gnss.ID = pos.ID_GnssPlace
left join dbo.FileLog fl on fl.ID = coalesce(pos.ID_FileLog, vui.ID_FileLog)
where (:occurredFrom is null or pos.Timestamp >= :occurredFrom)
and (:occurredTo is null or pos.Timestamp < :occurredTo)
)
select
cast(base.ID as varchar(128)) as source_row_id,
concat('TACHOGRAPH:VU_POSITION:', base.ID) as external_source_event_id,
base.occurred_at,
base.received_partner_at,
cast(base.Odo as bigint) * 1000 as odometer_m,
base.latitude,
base.longitude,
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
base.driver_card_nation,
base.driver_card_number,
cast(base.vehicle_identification_id as varchar(128)) as vehicle_source_entity_id,
base.vehicle_vin,
cast(v.ID as varchar(128)) as vehicle_registration_source_entity_id,
vn.AlphaCode as vehicle_registration_nation,
v.VRN as vehicle_registration_number,
'VEHICLE_UNIT' as source_package_kind,
cast(base.source_package_id_raw as varchar(128)) as source_package_id,
cast(base.source_package_entity_id_raw as varchar(128)) as source_package_entity_id,
base.source_package_period_from,
base.source_package_period_to,
base.source_package_imported_at
from Base base
outer apply (
select top 1 vehicle.ID,
vehicle.VRN,
vehicle.ID_Nation
from dbo.Vehicle vehicle
where vehicle.ID_VehicleIdentification = base.vehicle_identification_id
and (vehicle.ValidFrom is null or vehicle.ValidFrom <= base.occurred_at)
and (vehicle.ValidTo is null or vehicle.ValidTo > base.occurred_at)
order by
vehicle.ValidFrom desc,
vehicle.ID desc
) v
left join dbo.Nation vn on vn.ID = v.ID_Nation
where (
:organisationId is null
or exists (
select 1
from dbo.Vehicle_I_90021 rel
join OrgTree on OrgTree.I_90021_OID = rel.ID_I_90021
where rel.ID_Vehicle = v.ID
and rel.GILT_BIS is null
)
)

View File

@ -38,7 +38,7 @@ select
base.occurred_at,
base.received_partner_at,
case when base.condition_code in (3, 4) then 'FERRY_TRAIN' else 'OUT_OF_SCOPE' end as event_domain,
'SPECIFIC_CONDITION' as event_domain,
case when base.condition_code in (3, 4) then 'FERRY_TRAIN' else 'OUT' end as event_type,
case when base.condition_code in (1, 3) then 'BEGIN' else 'END' end as lifecycle,

View File

@ -28,23 +28,29 @@ class TachographImportPlanServiceTest {
new VuLoadUnloadRowMapper(detailsFactory),
new CardLoadUnloadRowMapper(detailsFactory),
new VuSpecificConditionRowMapper(detailsFactory),
new CardSpecificConditionRowMapper(detailsFactory)
new CardSpecificConditionRowMapper(detailsFactory),
new VuPositionRowMapper(detailsFactory),
new CardPositionRowMapper(detailsFactory),
new VuPlaceRowMapper(detailsFactory),
new CardPlaceRowMapper(detailsFactory)
);
@Test
void rejectsUnsupportedEventFamiliesWhenJdbcExtractionIsEnabled() {
TachographImportPlanService service = serviceWithJdbcExtractor();
TachographImportRequest request = requestForFamilies(EventFamily.POSITION);
TachographImportRequest request = requestForFamilies(EventFamily.SPEEDING);
assertThatThrownBy(() -> service.createPlan(request))
.isInstanceOf(UnsupportedTachographExtractionException.class)
.hasMessageContaining("VU_POSITION")
.hasMessageContaining("SPEEDING_EVENTS")
.hasMessageContaining("Supported JDBC extraction codes")
.hasMessageContaining("DRIVER_ACTIVITY")
.hasMessageContaining("DRIVER_CARD")
.hasMessageContaining("BORDER_CROSSING")
.hasMessageContaining("LOAD_UNLOAD")
.hasMessageContaining("SPECIFIC_CONDITION");
.hasMessageContaining("SPECIFIC_CONDITION")
.hasMessageContaining("POSITION")
.hasMessageContaining("PLACE");
}
@Test
@ -107,6 +113,30 @@ class TachographImportPlanServiceTest {
.containsExactlyInAnyOrder("VU_SPECIFIC_CONDITION", "CARD_SPECIFIC_CONDITION");
}
@Test
void allowsSupportedPositionFamilyWhenJdbcExtractionIsEnabled() {
TachographImportPlanService service = serviceWithJdbcExtractor();
TachographImportRequest request = requestForFamilies(EventFamily.POSITION);
var plan = service.createPlan(request);
assertThat(plan.items())
.extracting(item -> item.extractionCode())
.containsExactlyInAnyOrder("VU_POSITION", "CARD_POSITION");
}
@Test
void allowsSupportedPlaceFamilyWhenJdbcExtractionIsEnabled() {
TachographImportPlanService service = serviceWithJdbcExtractor();
TachographImportRequest request = requestForFamilies(EventFamily.PLACE);
var plan = service.createPlan(request);
assertThat(plan.items())
.extracting(item -> item.extractionCode())
.containsExactlyInAnyOrder("VU_PLACE", "CARD_PLACE");
}
private TachographImportPlanService serviceWithJdbcExtractor() {
EventHubProperties properties = new EventHubProperties();
properties.getTachograph().getDatasource().setJdbcUrl("jdbc:sqlserver://tachograph-db");