From 9d9541bac915058173bd601e9c54c31d233358c1 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Fri, 1 May 2026 10:34:01 +0200 Subject: [PATCH] Add tachograph place and position extractors --- README.md | 12 +- .../at/procon/eventhub/dto/EventDomain.java | 3 +- .../at/procon/eventhub/dto/EventType.java | 3 +- .../eventhub/service/EventDetailsFactory.java | 7 + .../service/EventHubEventValidator.java | 9 + .../AbstractTachographPlaceRowMapper.java | 239 ++++++++++++++++++ .../AbstractTachographPositionRowMapper.java | 201 +++++++++++++++ ...tTachographSpecificConditionRowMapper.java | 2 +- .../service/CardPlaceRowMapper.java | 12 + .../service/CardPositionRowMapper.java | 12 + ...achographExtractionDefinitionRegistry.java | 38 ++- .../service/TachographImportPlanService.java | 4 +- .../tachograph/service/VuPlaceRowMapper.java | 12 + .../service/VuPositionRowMapper.java | 12 + .../resources/sql/tachograph/card-place.sql | 100 ++++++++ .../sql/tachograph/card-position.sql | 90 +++++++ .../tachograph/card-specific-condition.sql | 2 +- .../resources/sql/tachograph/vu-place.sql | 101 ++++++++ .../resources/sql/tachograph/vu-position.sql | 94 +++++++ .../sql/tachograph/vu-specific-condition.sql | 2 +- .../TachographImportPlanServiceTest.java | 38 ++- 21 files changed, 977 insertions(+), 16 deletions(-) create mode 100644 src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographPlaceRowMapper.java create mode 100644 src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographPositionRowMapper.java create mode 100644 src/main/java/at/procon/eventhub/tachograph/service/CardPlaceRowMapper.java create mode 100644 src/main/java/at/procon/eventhub/tachograph/service/CardPositionRowMapper.java create mode 100644 src/main/java/at/procon/eventhub/tachograph/service/VuPlaceRowMapper.java create mode 100644 src/main/java/at/procon/eventhub/tachograph/service/VuPositionRowMapper.java create mode 100644 src/main/resources/sql/tachograph/card-place.sql create mode 100644 src/main/resources/sql/tachograph/card-position.sql create mode 100644 src/main/resources/sql/tachograph/vu-place.sql create mode 100644 src/main/resources/sql/tachograph/vu-position.sql diff --git a/README.md b/README.md index 0a4b2a4..3d585ef 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/src/main/java/at/procon/eventhub/dto/EventDomain.java b/src/main/java/at/procon/eventhub/dto/EventDomain.java index bfc900d..a09b75c 100644 --- a/src/main/java/at/procon/eventhub/dto/EventDomain.java +++ b/src/main/java/at/procon/eventhub/dto/EventDomain.java @@ -7,8 +7,7 @@ public enum EventDomain { POSITION, BORDER_CROSSING, LOAD_UNLOAD, - OUT_OF_SCOPE, - FERRY_TRAIN, + SPECIFIC_CONDITION, SPEEDING, PLACE, VEHICLE_DATA, diff --git a/src/main/java/at/procon/eventhub/dto/EventType.java b/src/main/java/at/procon/eventhub/dto/EventType.java index 384614c..12fecef 100644 --- a/src/main/java/at/procon/eventhub/dto/EventType.java +++ b/src/main/java/at/procon/eventhub/dto/EventType.java @@ -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, diff --git a/src/main/java/at/procon/eventhub/service/EventDetailsFactory.java b/src/main/java/at/procon/eventhub/service/EventDetailsFactory.java index a6e2851..8e25dfb 100644 --- a/src/main/java/at/procon/eventhub/service/EventDetailsFactory.java +++ b/src/main/java/at/procon/eventhub/service/EventDetailsFactory.java @@ -50,6 +50,13 @@ public class EventDetailsFactory { return new EventDetailsDto("POSITION", objectMapper.valueToTree(attributes)); } + public EventDetailsDto place(String country, String region) { + Map 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 attributes = new LinkedHashMap<>(); put(attributes, "countryFrom", countryFrom); diff --git a/src/main/java/at/procon/eventhub/service/EventHubEventValidator.java b/src/main/java/at/procon/eventhub/service/EventHubEventValidator.java index c50b501..7fc79cd 100644 --- a/src/main/java/at/procon/eventhub/service/EventHubEventValidator.java +++ b/src/main/java/at/procon/eventhub/service/EventHubEventValidator.java @@ -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"); } diff --git a/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographPlaceRowMapper.java b/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographPlaceRowMapper.java new file mode 100644 index 0000000..015e0f4 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographPlaceRowMapper.java @@ -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 { + + private final EventDetailsFactory detailsFactory; + + protected AbstractTachographPlaceRowMapper(EventDetailsFactory detailsFactory) { + this.detailsFactory = detailsFactory; + } + + @Override + public EventHubEventDto map(ResultSet rs, int rowNum, ExtractionContext 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 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 payload(ResultSet rs) throws SQLException { + Map raw = new LinkedHashMap<>(); + put(raw, "sourceRowId", string(rs, "source_row_id")); + raw.putAll(sourceSpecificPayload(rs)); + return Map.of("raw", raw); + } + + private String defaultExternalSourceEventId( + ExtractionContext 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 parseEnum(Class 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 target, String key, Object value) { + if (value != null) { + target.put(key, value); + } + } +} diff --git a/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographPositionRowMapper.java b/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographPositionRowMapper.java new file mode 100644 index 0000000..e16d0c2 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographPositionRowMapper.java @@ -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 { + + private final EventDetailsFactory detailsFactory; + + protected AbstractTachographPositionRowMapper(EventDetailsFactory detailsFactory) { + this.detailsFactory = detailsFactory; + } + + @Override + public EventHubEventDto map(ResultSet rs, int rowNum, ExtractionContext 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 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 payload(ResultSet rs) throws SQLException { + Map raw = new LinkedHashMap<>(); + put(raw, "sourceRowId", string(rs, "source_row_id")); + raw.putAll(sourceSpecificPayload(rs)); + return Map.of("raw", raw); + } + + private String defaultExternalSourceEventId( + ExtractionContext 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 target, String key, Object value) { + if (value != null) { + target.put(key, value); + } + } +} diff --git a/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographSpecificConditionRowMapper.java b/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographSpecificConditionRowMapper.java index 196f3d2..de9a3b3 100644 --- a/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographSpecificConditionRowMapper.java +++ b/src/main/java/at/procon/eventhub/tachograph/service/AbstractTachographSpecificConditionRowMapper.java @@ -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 { diff --git a/src/main/java/at/procon/eventhub/tachograph/service/CardPlaceRowMapper.java b/src/main/java/at/procon/eventhub/tachograph/service/CardPlaceRowMapper.java new file mode 100644 index 0000000..75a4cc1 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachograph/service/CardPlaceRowMapper.java @@ -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); + } +} diff --git a/src/main/java/at/procon/eventhub/tachograph/service/CardPositionRowMapper.java b/src/main/java/at/procon/eventhub/tachograph/service/CardPositionRowMapper.java new file mode 100644 index 0000000..c1c80c5 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachograph/service/CardPositionRowMapper.java @@ -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); + } +} diff --git a/src/main/java/at/procon/eventhub/tachograph/service/TachographExtractionDefinitionRegistry.java b/src/main/java/at/procon/eventhub/tachograph/service/TachographExtractionDefinitionRegistry.java index 7ba0f2f..a0e06a6 100644 --- a/src/main/java/at/procon/eventhub/tachograph/service/TachographExtractionDefinitionRegistry.java +++ b/src/main/java/at/procon/eventhub/tachograph/service/TachographExtractionDefinitionRegistry.java @@ -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 ) )); } diff --git a/src/main/java/at/procon/eventhub/tachograph/service/TachographImportPlanService.java b/src/main/java/at/procon/eventhub/tachograph/service/TachographImportPlanService.java index 416898b..689c27c 100644 --- a/src/main/java/at/procon/eventhub/tachograph/service/TachographImportPlanService.java +++ b/src/main/java/at/procon/eventhub/tachograph/service/TachographImportPlanService.java @@ -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), diff --git a/src/main/java/at/procon/eventhub/tachograph/service/VuPlaceRowMapper.java b/src/main/java/at/procon/eventhub/tachograph/service/VuPlaceRowMapper.java new file mode 100644 index 0000000..0f81c5a --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachograph/service/VuPlaceRowMapper.java @@ -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); + } +} diff --git a/src/main/java/at/procon/eventhub/tachograph/service/VuPositionRowMapper.java b/src/main/java/at/procon/eventhub/tachograph/service/VuPositionRowMapper.java new file mode 100644 index 0000000..fb5a80c --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachograph/service/VuPositionRowMapper.java @@ -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); + } +} diff --git a/src/main/resources/sql/tachograph/card-place.sql b/src/main/resources/sql/tachograph/card-place.sql new file mode 100644 index 0000000..9c37432 --- /dev/null +++ b/src/main/resources/sql/tachograph/card-place.sql @@ -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 diff --git a/src/main/resources/sql/tachograph/card-position.sql b/src/main/resources/sql/tachograph/card-position.sql new file mode 100644 index 0000000..a1fec8e --- /dev/null +++ b/src/main/resources/sql/tachograph/card-position.sql @@ -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 diff --git a/src/main/resources/sql/tachograph/card-specific-condition.sql b/src/main/resources/sql/tachograph/card-specific-condition.sql index 958e7de..b2590be 100644 --- a/src/main/resources/sql/tachograph/card-specific-condition.sql +++ b/src/main/resources/sql/tachograph/card-specific-condition.sql @@ -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, diff --git a/src/main/resources/sql/tachograph/vu-place.sql b/src/main/resources/sql/tachograph/vu-place.sql new file mode 100644 index 0000000..f24e0d5 --- /dev/null +++ b/src/main/resources/sql/tachograph/vu-place.sql @@ -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 + ) + ) diff --git a/src/main/resources/sql/tachograph/vu-position.sql b/src/main/resources/sql/tachograph/vu-position.sql new file mode 100644 index 0000000..1077be8 --- /dev/null +++ b/src/main/resources/sql/tachograph/vu-position.sql @@ -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 + ) + ) diff --git a/src/main/resources/sql/tachograph/vu-specific-condition.sql b/src/main/resources/sql/tachograph/vu-specific-condition.sql index fe58061..6031660 100644 --- a/src/main/resources/sql/tachograph/vu-specific-condition.sql +++ b/src/main/resources/sql/tachograph/vu-specific-condition.sql @@ -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, diff --git a/src/test/java/at/procon/eventhub/tachograph/service/TachographImportPlanServiceTest.java b/src/test/java/at/procon/eventhub/tachograph/service/TachographImportPlanServiceTest.java index 88de3ac..970961f 100644 --- a/src/test/java/at/procon/eventhub/tachograph/service/TachographImportPlanServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachograph/service/TachographImportPlanServiceTest.java @@ -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");