diff --git a/postman/eventhub-esper-poc.postman_collection.json b/postman/eventhub-esper-poc.postman_collection.json index e4502d0..a675bd3 100644 --- a/postman/eventhub-esper-poc.postman_collection.json +++ b/postman/eventhub-esper-poc.postman_collection.json @@ -351,6 +351,29 @@ } } }, + { + "name": "Get tachograph file session driver Esper events", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/eventhub/tachograph-file-sessions/{{sessionId}}/drivers/{{driverKey}}/processing/esper-events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "tachograph-file-sessions", + "{{sessionId}}", + "drivers", + "{{driverKey}}", + "processing", + "esper-events" + ] + } + } + }, { "name": "Process tachograph file session operating periods", "request": { diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionController.java b/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionController.java index 01543dd..46a8da3 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionController.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionController.java @@ -1,6 +1,7 @@ package at.procon.eventhub.tachographfilesession.api; import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; @@ -66,6 +67,14 @@ public class TachographFileSessionController { return ResponseEntity.ok(service.getDriver(sessionId, driverKey)); } + @GetMapping("/{sessionId}/drivers/{driverKey}/processing/esper-events") + public ResponseEntity getEsperDriverProcessingResults( + @PathVariable UUID sessionId, + @PathVariable String driverKey + ) { + return ResponseEntity.ok(processingService.getEsperDriverProcessingResults(sessionId, driverKey)); + } + @PostMapping("/{sessionId}/drivers/{driverKey}/processing/operating-periods") public ResponseEntity evaluateOperatingPeriods( @PathVariable UUID sessionId, diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java new file mode 100644 index 0000000..a1fd860 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java @@ -0,0 +1,26 @@ +package at.procon.eventhub.tachographfilesession.dto; + +import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public record TachographEsperDriverProcessingResultDto( + UUID sessionId, + String driverKey, + String sourceKind, + OffsetDateTime loadedFrom, + OffsetDateTime loadedTo, + int activityIntervalCount, + int drivingIntervalCount, + int vehicleUsageIntervalCount, + int vuCardAbsentIntervalCount, + List activityIntervals, + List drivingIntervals, + List vehicleUsageIntervals, + List vuCardAbsentIntervals, + List notes +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/ResolvedVehicleUsageInterval.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/ResolvedVehicleUsageInterval.java index e0080e5..9d7b2a9 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/ResolvedVehicleUsageInterval.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/ResolvedVehicleUsageInterval.java @@ -3,8 +3,11 @@ package at.procon.eventhub.tachographfilesession.model; import java.time.Duration; import java.time.OffsetDateTime; import java.util.List; +import java.util.UUID; public record ResolvedVehicleUsageInterval( + UUID sessionId, + String driverKey, String intervalId, OffsetDateTime from, OffsetDateTime to, @@ -17,6 +20,8 @@ public record ResolvedVehicleUsageInterval( List sourceIntervalIds ) { public static ResolvedVehicleUsageInterval resolved( + UUID sessionId, + String driverKey, String intervalId, OffsetDateTime from, OffsetDateTime to, @@ -28,6 +33,8 @@ public record ResolvedVehicleUsageInterval( List sourceIntervalIds ) { return new ResolvedVehicleUsageInterval( + sessionId, + driverKey, intervalId, from, to, diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperActivityIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperActivityIntervalEvent.java new file mode 100644 index 0000000..d7f8518 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperActivityIntervalEvent.java @@ -0,0 +1,26 @@ +package at.procon.eventhub.tachographfilesession.model; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public record TachographEsperActivityIntervalEvent( + UUID sessionId, + String driverKey, + String intervalId, + String activityType, + String cardSlot, + String cardStatus, + String drivingStatus, + String registrationKey, + String vehicleKey, + String sourceKind, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + long durationSeconds, + List sourceIntervalIds, + boolean synthetic, + boolean clippedToRequestedPeriod, + String level +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperVehicleUsageIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperVehicleUsageIntervalEvent.java new file mode 100644 index 0000000..f9cfffa --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperVehicleUsageIntervalEvent.java @@ -0,0 +1,21 @@ +package at.procon.eventhub.tachographfilesession.model; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public record TachographEsperVehicleUsageIntervalEvent( + UUID sessionId, + String driverKey, + String intervalId, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + long durationSeconds, + Long odometerBeginKm, + Long odometerEndKm, + String registrationKey, + String vehicleKey, + String sourceKind, + List sourceIntervalIds +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperVuCardAbsentIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperVuCardAbsentIntervalEvent.java new file mode 100644 index 0000000..f84cb9a --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperVuCardAbsentIntervalEvent.java @@ -0,0 +1,19 @@ +package at.procon.eventhub.tachographfilesession.model; + +import java.time.OffsetDateTime; +import java.util.UUID; + +public record TachographEsperVuCardAbsentIntervalEvent( + UUID sessionId, + String driverKey, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + long durationSeconds, + String previousUsageIntervalId, + String nextUsageIntervalId, + String previousRegistrationKey, + String nextRegistrationKey, + String previousVehicleKey, + String nextVehicleKey +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java index 06f983f..70fe361 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java @@ -1,5 +1,15 @@ package at.procon.eventhub.tachographfilesession.service; +import com.espertech.esper.common.client.EPCompiled; +import com.espertech.esper.common.client.EventBean; +import com.espertech.esper.common.client.configuration.Configuration; +import com.espertech.esper.compiler.client.CompilerArguments; +import com.espertech.esper.compiler.client.EPCompileException; +import com.espertech.esper.compiler.client.EPCompilerProvider; +import com.espertech.esper.runtime.client.EPDeployException; +import com.espertech.esper.runtime.client.EPDeployment; +import com.espertech.esper.runtime.client.EPRuntime; +import com.espertech.esper.runtime.client.EPRuntimeProvider; import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval; import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval; @@ -8,22 +18,48 @@ import at.procon.eventhub.tachographfilesession.model.ExtractionWarning; import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; +import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; -import java.time.Duration; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; @Component public class DriverTimelineBuilder { + private static final AtomicLong RUNTIME_COUNTER = new AtomicLong(); + private static final String ACTIVITY_INTERVAL_EVENTS_EPL = + loadResource("esper/tachograph-activity-interval-events.epl"); + private static final String DRIVING_INTERVAL_EVENTS_EPL = + loadResource("esper/tachograph-driving-interval-events.epl"); + private static final String VEHICLE_USAGE_INTERVAL_EVENTS_EPL = + loadResource("esper/tachograph-vehicle-usage-interval-events.epl"); + private static final String VU_CARD_ABSENT_INTERVAL_EVENTS_EPL = + loadResource("esper/tachograph-vu-card-absent-interval-events.epl"); + public ResolvedDriverTimeline build(TachographFileSession session, DriverExtractionSession driverSession) { String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT"; - List vehicleUsageIntervals = mergeVehicleUsageIntervals(driverSession.cardVehicleUsageIntervals(), sourceKind); + List vehicleUsageIntervals = mergeVehicleUsageIntervals( + session.sessionId(), + driverSession.driverKey(), + driverSession.cardVehicleUsageIntervals(), + sourceKind + ); List activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind); List supportEvents = driverSession.supportEvents().stream() .sorted(Comparator.comparing(ExtractedSupportEvent::occurredAt) @@ -44,7 +80,200 @@ public class DriverTimelineBuilder { ); } + public List buildEsperActivityIntervalEvents( + TachographFileSession session, + DriverExtractionSession driverSession + ) { + String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT"; + List activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind); + return buildEsperActivityIntervalEvents(session.sessionId(), driverSession.driverKey(), activityIntervals); + } + + public List buildEsperActivityIntervalEvents( + UUID sessionId, + String driverKey, + ResolvedDriverTimeline timeline + ) { + return timeline == null + ? List.of() + : buildEsperActivityIntervalEvents(sessionId, driverKey, timeline.activityIntervals()); + } + + public List buildEsperDrivingIntervalEvents( + TachographFileSession session, + DriverExtractionSession driverSession + ) { + String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT"; + List activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind); + return buildEsperDrivingIntervalEvents(session.sessionId(), driverSession.driverKey(), activityIntervals); + } + + public List buildEsperDrivingIntervalEvents( + UUID sessionId, + String driverKey, + ResolvedDriverTimeline timeline + ) { + if (timeline == null) { + return List.of(); + } + return buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline.activityIntervals()); + } + + public List buildEsperVehicleUsageIntervalEvents( + TachographFileSession session, + DriverExtractionSession driverSession + ) { + String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT"; + List vehicleUsageIntervals = mergeVehicleUsageIntervals( + session.sessionId(), + driverSession.driverKey(), + driverSession.cardVehicleUsageIntervals(), + sourceKind + ); + return buildEsperVehicleUsageIntervalEvents(vehicleUsageIntervals); + } + + public List buildEsperVehicleUsageIntervalEvents( + ResolvedDriverTimeline timeline + ) { + return timeline == null ? List.of() : buildEsperVehicleUsageIntervalEvents(timeline.vehicleUsageIntervals()); + } + + public List buildEsperVuCardAbsentIntervalEvents( + TachographFileSession session, + DriverExtractionSession driverSession + ) { + String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT"; + List vehicleUsageIntervals = mergeVehicleUsageIntervals( + session.sessionId(), + driverSession.driverKey(), + driverSession.cardVehicleUsageIntervals(), + sourceKind + ); + return buildEsperVuCardAbsentIntervalEvents(vehicleUsageIntervals); + } + + public List buildEsperVuCardAbsentIntervalEvents( + ResolvedDriverTimeline timeline + ) { + return timeline == null ? List.of() : buildEsperVuCardAbsentIntervalEvents(timeline.vehicleUsageIntervals()); + } + + private List buildEsperActivityIntervalEvents( + UUID sessionId, + String driverKey, + List activityIntervals + ) { + if (activityIntervals == null || activityIntervals.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(); + executeWithRuntime( + configuration -> configuration.getCommon().addEventType( + "TachographActivityIntervalInputEvent", + activityIntervalInputDefinition() + ), + ACTIVITY_INTERVAL_EVENTS_EPL, + "activityIntervals", + newData -> collectActivityIntervalEvents(newData, result), + runtime -> { + for (ResolvedActivityInterval interval : activityIntervals) { + runtime.getEventService().sendEventMap( + toActivityIntervalInputMap(sessionId, driverKey, interval), + "TachographActivityIntervalInputEvent" + ); + } + } + ); + return result; + } + + private List buildEsperDrivingIntervalEvents( + UUID sessionId, + String driverKey, + List activityIntervals + ) { + if (activityIntervals == null || activityIntervals.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(); + executeWithRuntime( + configuration -> configuration.getCommon().addEventType( + "TachographActivityIntervalInputEvent", + activityIntervalInputDefinition() + ), + DRIVING_INTERVAL_EVENTS_EPL, + "drivingIntervals", + newData -> collectActivityIntervalEvents(newData, result), + runtime -> { + for (ResolvedActivityInterval interval : activityIntervals) { + runtime.getEventService().sendEventMap( + toActivityIntervalInputMap(sessionId, driverKey, interval), + "TachographActivityIntervalInputEvent" + ); + } + } + ); + return result; + } + + private List buildEsperVehicleUsageIntervalEvents( + List vehicleUsageIntervals + ) { + if (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(); + executeWithRuntime( + configuration -> configuration.getCommon().addEventType( + "TachographVehicleUsageIntervalInputEvent", + vehicleUsageIntervalInputDefinition() + ), + VEHICLE_USAGE_INTERVAL_EVENTS_EPL, + "vehicleUsageIntervals", + newData -> collectVehicleUsageIntervalEvents(newData, result), + runtime -> { + for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) { + runtime.getEventService().sendEventMap( + toVehicleUsageIntervalInputMap(interval), + "TachographVehicleUsageIntervalInputEvent" + ); + } + } + ); + return result; + } + + private List buildEsperVuCardAbsentIntervalEvents( + List vehicleUsageIntervals + ) { + if (vehicleUsageIntervals == null || vehicleUsageIntervals.size() < 2) { + return List.of(); + } + List result = new ArrayList<>(); + executeWithRuntime( + configuration -> configuration.getCommon().addEventType( + "TachographVehicleUsageIntervalInputEvent", + vehicleUsageIntervalInputDefinition() + ), + VU_CARD_ABSENT_INTERVAL_EVENTS_EPL, + "vuCardAbsentIntervals", + newData -> collectVuCardAbsentIntervalEvents(newData, result), + runtime -> { + for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) { + runtime.getEventService().sendEventMap( + toVehicleUsageIntervalInputMap(interval), + "TachographVehicleUsageIntervalInputEvent" + ); + } + } + ); + return result; + } + private List mergeVehicleUsageIntervals( + UUID sessionId, + String driverKey, List rawIntervals, String sourceKind ) { @@ -54,6 +283,8 @@ public class DriverTimelineBuilder { List sorted = rawIntervals.stream() .filter(interval -> interval.from() != null && (interval.to() == null || interval.to().isAfter(interval.from()))) .map(interval -> ResolvedVehicleUsageInterval.resolved( + sessionId, + driverKey, interval.intervalId(), interval.from(), interval.to(), @@ -79,6 +310,8 @@ public class DriverTimelineBuilder { if (canMerge(current, next)) { currentSources.addAll(next.sourceIntervalIds()); current = ResolvedVehicleUsageInterval.resolved( + current.sessionId(), + current.driverKey(), current.intervalId() + "+" + next.intervalId(), current.from(), mergedTo(current.to(), next.to()), @@ -220,4 +453,210 @@ public class DriverTimelineBuilder { private OffsetDateTime mergeBoundary(OffsetDateTime endInclusive) { return endInclusive == null ? OffsetDateTime.MAX : endInclusive.plusSeconds(1); } + + private void executeWithRuntime( + Consumer configurationSetup, + String epl, + String statementName, + Consumer listener, + Consumer sender + ) { + EPRuntime runtime = null; + try { + Configuration configuration = new Configuration(); + configurationSetup.accept(configuration); + String runtimeUri = "eventhub-tachograph-projection-" + RUNTIME_COUNTER.incrementAndGet(); + runtime = EPRuntimeProvider.getRuntime(runtimeUri, configuration); + + CompilerArguments arguments = new CompilerArguments(configuration); + EPCompiled compiled = EPCompilerProvider.getCompiler().compile(epl, arguments); + EPDeployment deployment = runtime.getDeploymentService().deploy(compiled); + runtime.getDeploymentService() + .getStatement(deployment.getDeploymentId(), statementName) + .addListener((newData, oldData, statement, rt) -> listener.accept(newData)); + + sender.accept(runtime); + } catch (EPCompileException | EPDeployException e) { + throw new IllegalStateException("Cannot compile/deploy tachograph projection EPL", e); + } finally { + if (runtime != null) { + runtime.destroy(); + } + } + } + + private Map activityIntervalInputDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("intervalId", String.class); + definition.put("activityType", String.class); + definition.put("cardSlot", String.class); + definition.put("cardStatus", String.class); + definition.put("drivingStatus", String.class); + definition.put("registrationKey", String.class); + definition.put("vehicleKey", String.class); + definition.put("sourceKind", String.class); + definition.put("startedAt", OffsetDateTime.class); + definition.put("endedAt", OffsetDateTime.class); + definition.put("durationSeconds", long.class); + definition.put("sourceIntervalIds", java.util.List.class); + definition.put("synthetic", boolean.class); + definition.put("clippedToRequestedPeriod", boolean.class); + definition.put("level", String.class); + return definition; + } + + private Map vehicleUsageIntervalInputDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("intervalId", String.class); + definition.put("startedAt", OffsetDateTime.class); + definition.put("endedAt", OffsetDateTime.class); + definition.put("durationSeconds", long.class); + definition.put("odometerBeginKm", Long.class); + definition.put("odometerEndKm", Long.class); + definition.put("registrationKey", String.class); + definition.put("vehicleKey", String.class); + definition.put("sourceKind", String.class); + definition.put("sourceIntervalIds", java.util.List.class); + return definition; + } + + private Map toActivityIntervalInputMap( + UUID sessionId, + String driverKey, + ResolvedActivityInterval interval + ) { + Map event = new LinkedHashMap<>(); + event.put("sessionId", sessionId); + event.put("driverKey", driverKey); + event.put("intervalId", interval.intervalId()); + event.put("activityType", interval.activityType()); + event.put("cardSlot", interval.slot()); + event.put("cardStatus", interval.cardStatus()); + event.put("drivingStatus", interval.drivingStatus()); + event.put("registrationKey", interval.registrationKey()); + event.put("vehicleKey", interval.vehicleKey()); + event.put("sourceKind", interval.sourceKind()); + event.put("startedAt", interval.from()); + event.put("endedAt", interval.to()); + event.put("durationSeconds", interval.durationSeconds()); + event.put("sourceIntervalIds", interval.sourceIntervalIds()); + event.put("synthetic", interval.synthetic()); + event.put("clippedToRequestedPeriod", interval.clippedToRequestedPeriod()); + event.put("level", interval.level()); + return event; + } + + private Map toVehicleUsageIntervalInputMap(ResolvedVehicleUsageInterval interval) { + Map event = new LinkedHashMap<>(); + event.put("sessionId", interval.sessionId()); + event.put("driverKey", interval.driverKey()); + event.put("intervalId", interval.intervalId()); + event.put("startedAt", interval.from()); + event.put("endedAt", interval.to()); + event.put("durationSeconds", interval.durationSeconds()); + event.put("odometerBeginKm", interval.odometerBeginKm()); + event.put("odometerEndKm", interval.odometerEndKm()); + event.put("registrationKey", interval.registrationKey()); + event.put("vehicleKey", interval.vehicleKey()); + event.put("sourceKind", interval.sourceKind()); + event.put("sourceIntervalIds", interval.sourceIntervalIds()); + return event; + } + + private void collectActivityIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + target.add(new TachographEsperActivityIntervalEvent( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + (String) event.get("intervalId"), + (String) event.get("activityType"), + (String) event.get("cardSlot"), + (String) event.get("cardStatus"), + (String) event.get("drivingStatus"), + (String) event.get("registrationKey"), + (String) event.get("vehicleKey"), + (String) event.get("sourceKind"), + (OffsetDateTime) event.get("startedAt"), + (OffsetDateTime) event.get("endedAt"), + (Long) event.get("durationSeconds"), + castSourceIntervalIds(event.get("sourceIntervalIds")), + (Boolean) event.get("synthetic"), + (Boolean) event.get("clippedToRequestedPeriod"), + (String) event.get("level") + )); + } + } + + private void collectVehicleUsageIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + target.add(new TachographEsperVehicleUsageIntervalEvent( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + (String) event.get("intervalId"), + (OffsetDateTime) event.get("startedAt"), + (OffsetDateTime) event.get("endedAt"), + (Long) event.get("durationSeconds"), + (Long) event.get("odometerBeginKm"), + (Long) event.get("odometerEndKm"), + (String) event.get("registrationKey"), + (String) event.get("vehicleKey"), + (String) event.get("sourceKind"), + castSourceIntervalIds(event.get("sourceIntervalIds")) + )); + } + } + + private void collectVuCardAbsentIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + target.add(new TachographEsperVuCardAbsentIntervalEvent( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + (OffsetDateTime) event.get("startedAt"), + (OffsetDateTime) event.get("endedAt"), + (Long) event.get("durationSeconds"), + (String) event.get("previousUsageIntervalId"), + (String) event.get("nextUsageIntervalId"), + (String) event.get("previousRegistrationKey"), + (String) event.get("nextRegistrationKey"), + (String) event.get("previousVehicleKey"), + (String) event.get("nextVehicleKey") + )); + } + } + + @SuppressWarnings("unchecked") + private List castSourceIntervalIds(Object value) { + return value == null ? List.of() : List.copyOf((List) value); + } + + private static String loadResource(String path) { + try { + ClassPathResource resource = new ClassPathResource(path); + return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Cannot load EPL resource: " + path, e); + } + } } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java index 6073ae2..aa7bd57 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java @@ -1,6 +1,7 @@ package at.procon.eventhub.tachographfilesession.service; import at.procon.eventhub.config.EventHubProperties; +import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; @@ -10,7 +11,10 @@ import at.procon.eventhub.tachographfilesession.model.ProcessedOperatingPeriod; import at.procon.eventhub.tachographfilesession.model.ProcessedShiftDrivingEvaluation; import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import java.time.Duration; import java.time.OffsetDateTime; import java.util.ArrayList; @@ -113,6 +117,45 @@ public class TachographFileSessionProcessingService { ); } + public TachographEsperDriverProcessingResultDto getEsperDriverProcessingResults( + UUID sessionId, + String driverKey + ) { + TachographFileSession session = repository.find(sessionId) + .orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId)); + DriverExtractionSession driver = session.driversByKey().get(driverKey); + if (driver == null) { + throw new DriverNotFoundInSessionException(sessionId, driverKey); + } + + ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver); + List activityIntervals = + driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline); + List drivingIntervals = + driverTimelineBuilder.buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline); + List vehicleUsageIntervals = + driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline); + List vuCardAbsentIntervals = + driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline); + + return new TachographEsperDriverProcessingResultDto( + sessionId, + driverKey, + timeline.sourceKind(), + timeline.loadedFrom(), + timeline.loadedTo(), + activityIntervals.size(), + drivingIntervals.size(), + vehicleUsageIntervals.size(), + vuCardAbsentIntervals.size(), + activityIntervals, + drivingIntervals, + vehicleUsageIntervals, + vuCardAbsentIntervals, + esperProjectionNotes() + ); + } + private List synthesizeUnknownGaps( List knownIntervals, Duration gapDetectionTolerance @@ -660,6 +703,14 @@ public class TachographFileSessionProcessingService { ); } + private List esperProjectionNotes() { + return List.of( + "This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.", + "Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.", + "VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver." + ); + } + private OffsetDateTime utc(OffsetDateTime value) { return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC); } diff --git a/src/main/resources/esper/tachograph-activity-interval-events.epl b/src/main/resources/esper/tachograph-activity-interval-events.epl new file mode 100644 index 0000000..cd9e87e --- /dev/null +++ b/src/main/resources/esper/tachograph-activity-interval-events.epl @@ -0,0 +1,20 @@ +@name('activityIntervals') +select + sessionId, + driverKey, + intervalId, + activityType, + cardSlot, + cardStatus, + drivingStatus, + registrationKey, + vehicleKey, + sourceKind, + startedAt, + endedAt, + durationSeconds, + sourceIntervalIds, + synthetic, + clippedToRequestedPeriod, + level +from TachographActivityIntervalInputEvent diff --git a/src/main/resources/esper/tachograph-driving-interval-events.epl b/src/main/resources/esper/tachograph-driving-interval-events.epl new file mode 100644 index 0000000..c0e3a08 --- /dev/null +++ b/src/main/resources/esper/tachograph-driving-interval-events.epl @@ -0,0 +1,20 @@ +@name('drivingIntervals') +select + sessionId, + driverKey, + intervalId, + activityType, + cardSlot, + cardStatus, + drivingStatus, + registrationKey, + vehicleKey, + sourceKind, + startedAt, + endedAt, + durationSeconds, + sourceIntervalIds, + synthetic, + clippedToRequestedPeriod, + level +from TachographActivityIntervalInputEvent(activityType = 'DRIVE') diff --git a/src/main/resources/esper/tachograph-vehicle-usage-interval-events.epl b/src/main/resources/esper/tachograph-vehicle-usage-interval-events.epl new file mode 100644 index 0000000..d0e97b5 --- /dev/null +++ b/src/main/resources/esper/tachograph-vehicle-usage-interval-events.epl @@ -0,0 +1,15 @@ +@name('vehicleUsageIntervals') +select + sessionId, + driverKey, + intervalId, + startedAt, + endedAt, + durationSeconds, + odometerBeginKm, + odometerEndKm, + registrationKey, + vehicleKey, + sourceKind, + sourceIntervalIds +from TachographVehicleUsageIntervalInputEvent diff --git a/src/main/resources/esper/tachograph-vu-card-absent-interval-events.epl b/src/main/resources/esper/tachograph-vu-card-absent-interval-events.epl new file mode 100644 index 0000000..bd5ac2b --- /dev/null +++ b/src/main/resources/esper/tachograph-vu-card-absent-interval-events.epl @@ -0,0 +1,54 @@ +create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent; + +create schema VuCardAbsentInterval( + sessionId java.util.UUID, + driverKey string, + startedAt java.time.OffsetDateTime, + endedAt java.time.OffsetDateTime, + durationSeconds long, + previousUsageIntervalId string, + nextUsageIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +context PerDriver +create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent; + +context PerDriver +@Priority(30) +on TachographVehicleUsageIntervalInputEvent as next +insert into VuCardAbsentInterval +select + prev.sessionId as sessionId, + prev.driverKey as driverKey, + prev.endedAt.plusSeconds(1) as startedAt, + next.startedAt as endedAt, + java.time.Duration.between(prev.endedAt.plusSeconds(1), next.startedAt).getSeconds() as durationSeconds, + prev.intervalId as previousUsageIntervalId, + next.intervalId as nextUsageIntervalId, + prev.registrationKey as previousRegistrationKey, + next.registrationKey as nextRegistrationKey, + prev.vehicleKey as previousVehicleKey, + next.vehicleKey as nextVehicleKey +from PreviousVehicleUsageInterval as prev +where prev.endedAt is not null + and next.startedAt is not null + and next.startedAt.isAfter(prev.endedAt.plusSeconds(1)); + +context PerDriver +@Priority(20) +on TachographVehicleUsageIntervalInputEvent +delete from PreviousVehicleUsageInterval; + +context PerDriver +@Priority(10) +on TachographVehicleUsageIntervalInputEvent as current +insert into PreviousVehicleUsageInterval +select *; + +@name('vuCardAbsentIntervals') +context PerDriver +select * from VuCardAbsentInterval; diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java index 41b4465..e0b1755 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java @@ -9,6 +9,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; @@ -17,8 +18,12 @@ import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionDeleteR import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDriversResponse; import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto; import at.procon.eventhub.tachographfilesession.model.ExtractionStats; +import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionService; +import java.time.OffsetDateTime; import java.time.Instant; import java.util.List; import java.util.UUID; @@ -58,6 +63,84 @@ class TachographFileSessionControllerTest { when(service.getSession(sessionId)).thenReturn(summary); when(service.listDrivers(sessionId)).thenReturn(new TachographFileSessionListDriversResponse(sessionId, List.of(driver))); when(service.getDriver(sessionId, "12:123")).thenReturn(new TachographFileDriverDetailDto(sessionId, "12:123", null, null, List.of(), List.of(), List.of(), List.of(), List.of(), List.of())); + when(processingService.getEsperDriverProcessingResults(sessionId, "12:123")) + .thenReturn(new TachographEsperDriverProcessingResultDto( + sessionId, + "12:123", + "DRIVER_CARD", + OffsetDateTime.parse("2026-05-12T08:00:00Z"), + OffsetDateTime.parse("2026-05-12T12:00:00Z"), + 2, + 1, + 2, + 1, + List.of(new TachographEsperActivityIntervalEvent( + sessionId, + "12:123", + "ACT-1", + "WORK", + "DRIVER", + "INSERTED", + "SINGLE", + "12:REG-1", + "VIN-1", + "DRIVER_CARD", + OffsetDateTime.parse("2026-05-12T08:00:00Z"), + OffsetDateTime.parse("2026-05-12T09:00:00Z"), + 3600L, + List.of("ACT-1"), + false, + false, + "RAW_INTERVAL" + )), + List.of(new TachographEsperActivityIntervalEvent( + sessionId, + "12:123", + "ACT-2", + "DRIVE", + "DRIVER", + "INSERTED", + "SINGLE", + "12:REG-1", + "VIN-1", + "DRIVER_CARD", + OffsetDateTime.parse("2026-05-12T09:00:00Z"), + OffsetDateTime.parse("2026-05-12T10:00:00Z"), + 3600L, + List.of("ACT-2"), + false, + false, + "RAW_INTERVAL" + )), + List.of(new TachographEsperVehicleUsageIntervalEvent( + sessionId, + "12:123", + "CVU-1", + OffsetDateTime.parse("2026-05-12T08:00:00Z"), + OffsetDateTime.parse("2026-05-12T10:00:00Z"), + 7200L, + 100L, + 200L, + "12:REG-1", + "VIN-1", + "DRIVER_CARD", + List.of("CVU-1") + )), + List.of(new TachographEsperVuCardAbsentIntervalEvent( + sessionId, + "12:123", + OffsetDateTime.parse("2026-05-12T10:00:01Z"), + OffsetDateTime.parse("2026-05-12T11:00:00Z"), + 3599L, + "CVU-1", + "CVU-2", + "12:REG-1", + "12:REG-2", + "VIN-1", + "VIN-2" + )), + List.of("note") + )); when(processingService.evaluateOperatingPeriods(eq(sessionId), eq("12:123"), org.mockito.ArgumentMatchers.any(TachographOperatingPeriodsProcessingRequest.class))) .thenReturn(new TachographOperatingPeriodsProcessingResultDto( sessionId, @@ -104,6 +187,14 @@ class TachographFileSessionControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.driverKey").value("12:123")); + mockMvc.perform(get("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/esper-events", sessionId, "12:123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.driverKey").value("12:123")) + .andExpect(jsonPath("$.sourceKind").value("DRIVER_CARD")) + .andExpect(jsonPath("$.activityIntervalCount").value(2)) + .andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1)) + .andExpect(jsonPath("$.drivingIntervals[0].activityType").value("DRIVE")); + mockMvc.perform(post("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/operating-periods", sessionId, "12:123") .contentType("application/json") .content(""" diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java index 88db8fb..66c5be5 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java @@ -9,6 +9,9 @@ import at.procon.eventhub.tachographfilesession.model.ExtractionWarning; import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; import at.procon.eventhub.tachographfilesession.model.ExtractionStats; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata; import java.time.Instant; @@ -100,6 +103,8 @@ class DriverTimelineBuilderTest { assertThat(timeline.sourceKind()).isEqualTo("DRIVER_CARD"); assertThat(timeline.vehicleUsageIntervals()).hasSize(1); + assertThat(timeline.vehicleUsageIntervals().get(0).sessionId()).isEqualTo(session.sessionId()); + assertThat(timeline.vehicleUsageIntervals().get(0).driverKey()).isEqualTo("12:123"); assertThat(timeline.vehicleUsageIntervals().get(0).from()).isEqualTo(OffsetDateTime.parse("2026-05-01T00:00:00Z")); assertThat(timeline.vehicleUsageIntervals().get(0).to()).isEqualTo(OffsetDateTime.parse("2026-05-02T08:00:00Z")); assertThat(timeline.activityIntervals()).hasSize(1); @@ -169,9 +174,205 @@ class DriverTimelineBuilderTest { ResolvedDriverTimeline timeline = builder.build(session, driver); assertThat(timeline.vehicleUsageIntervals()).hasSize(1); + assertThat(timeline.vehicleUsageIntervals().get(0).sessionId()).isEqualTo(session.sessionId()); + assertThat(timeline.vehicleUsageIntervals().get(0).driverKey()).isEqualTo("12:123"); assertThat(timeline.vehicleUsageIntervals().get(0).from()).isEqualTo(OffsetDateTime.parse("2026-05-01T00:00:00Z")); assertThat(timeline.vehicleUsageIntervals().get(0).to()).isNull(); assertThat(timeline.vehicleUsageIntervals().get(0).sourceIntervalIds()).containsExactly("CVU-1", "CVU-2"); assertThat(timeline.loadedTo()).isEqualTo(OffsetDateTime.parse("2026-05-02T09:00:00Z")); } + + @Test + void buildsEsperActivityAndDrivingIntervalEventsFromResolvedTimeline() { + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + null, + null, + List.of(), + List.of(), + List.of(), + List.of( + new ExtractedCardActivityInterval( + "ACT-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T09:00:00Z"), + "WORK", + "DRIVER", + "INSERTED", + "SINGLE", + "12:REG-1", + "VIN-1", + "a" + ), + new ExtractedCardActivityInterval( + "ACT-2", + OffsetDateTime.parse("2026-05-01T09:00:00Z"), + OffsetDateTime.parse("2026-05-01T10:30:00Z"), + "DRIVE", + "DRIVER", + "INSERTED", + "SINGLE", + "12:REG-1", + "VIN-1", + "b" + ) + ), + List.of(), + List.of() + ); + TachographFileSession session = new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "42", "b", true, null), + Map.of(driver.driverKey(), driver), + new ExtractionStats(1, 2, 0, 0, 0, 0), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + + List activityEvents = + builder.buildEsperActivityIntervalEvents(session, driver); + List drivingEvents = + builder.buildEsperDrivingIntervalEvents(session, driver); + + assertThat(activityEvents).hasSize(2); + assertThat(activityEvents).extracting(TachographEsperActivityIntervalEvent::activityType) + .containsExactly("WORK", "DRIVE"); + assertThat(activityEvents).extracting(TachographEsperActivityIntervalEvent::driverKey) + .containsOnly("12:123"); + + assertThat(drivingEvents).hasSize(1); + assertThat(drivingEvents.get(0).intervalId()).isEqualTo("ACT-2"); + assertThat(drivingEvents.get(0).activityType()).isEqualTo("DRIVE"); + assertThat(drivingEvents.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z")); + assertThat(drivingEvents.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:30:00Z")); + } + + @Test + void buildsEsperVehicleUsageIntervalEventsFromResolvedTimeline() { + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + null, + null, + List.of(), + List.of(), + List.of( + new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T11:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "a" + ), + new ExtractedCardVehicleUsageInterval( + "CVU-2", + OffsetDateTime.parse("2026-05-01T12:00:00Z"), + null, + 201L, + null, + "12:REG-1", + "VIN-1", + "b" + ) + ), + List.of(), + List.of(), + List.of() + ); + TachographFileSession session = new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "42", "b", true, null), + Map.of(driver.driverKey(), driver), + new ExtractionStats(1, 0, 2, 0, 0, 0), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + + List vehicleUsageEvents = + builder.buildEsperVehicleUsageIntervalEvents(session, driver); + + assertThat(vehicleUsageEvents).hasSize(2); + assertThat(vehicleUsageEvents).extracting(TachographEsperVehicleUsageIntervalEvent::driverKey) + .containsOnly("12:123"); + assertThat(vehicleUsageEvents).extracting(TachographEsperVehicleUsageIntervalEvent::sessionId) + .containsOnly(session.sessionId()); + assertThat(vehicleUsageEvents.get(0).intervalId()).isEqualTo("CVU-1"); + assertThat(vehicleUsageEvents.get(1).intervalId()).isEqualTo("CVU-2"); + assertThat(vehicleUsageEvents.get(1).endedAt()).isNull(); + } + + @Test + void buildsEsperVuCardAbsentIntervalEventsFromVehicleUsageGaps() { + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + null, + null, + List.of(), + List.of(), + List.of( + new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T11:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "a" + ), + new ExtractedCardVehicleUsageInterval( + "CVU-2", + OffsetDateTime.parse("2026-05-01T12:00:00Z"), + OffsetDateTime.parse("2026-05-01T13:00:00Z"), + 201L, + 260L, + "12:REG-2", + "VIN-2", + "b" + ), + new ExtractedCardVehicleUsageInterval( + "CVU-3", + OffsetDateTime.parse("2026-05-01T13:00:01Z"), + OffsetDateTime.parse("2026-05-01T14:00:00Z"), + 261L, + 320L, + "12:REG-2", + "VIN-2", + "c" + ) + ), + List.of(), + List.of(), + List.of() + ); + TachographFileSession session = new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null), + Map.of(driver.driverKey(), driver), + new ExtractionStats(1, 0, 3, 0, 0, 0), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + + List absentIntervals = + builder.buildEsperVuCardAbsentIntervalEvents(session, driver); + + assertThat(absentIntervals).hasSize(1); + assertThat(absentIntervals.get(0).sessionId()).isEqualTo(session.sessionId()); + assertThat(absentIntervals.get(0).driverKey()).isEqualTo("12:123"); + assertThat(absentIntervals.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:01Z")); + assertThat(absentIntervals.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:00:00Z")); + assertThat(absentIntervals.get(0).durationSeconds()).isEqualTo(3599L); + assertThat(absentIntervals.get(0).previousUsageIntervalId()).isEqualTo("CVU-1"); + assertThat(absentIntervals.get(0).nextUsageIntervalId()).isEqualTo("CVU-2"); + assertThat(absentIntervals.get(0).previousRegistrationKey()).isEqualTo("12:REG-1"); + assertThat(absentIntervals.get(0).nextRegistrationKey()).isEqualTo("12:REG-2"); + assertThat(absentIntervals.get(0).previousVehicleKey()).isEqualTo("VIN-1"); + assertThat(absentIntervals.get(0).nextVehicleKey()).isEqualTo("VIN-2"); + } } diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java index 08b6055..503566b 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java @@ -3,6 +3,7 @@ package at.procon.eventhub.tachographfilesession.service; import static org.assertj.core.api.Assertions.assertThat; import at.procon.eventhub.config.EventHubProperties; +import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; @@ -21,6 +22,74 @@ import org.junit.jupiter.api.Test; class TachographFileSessionProcessingServiceTest { + @Test + void returnsEsperDriverProcessingResultsFromSessionTimeline() { + EventHubProperties properties = new EventHubProperties(); + TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties); + TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( + repository, + new DriverTimelineBuilder(), + properties + ); + + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + null, + null, + List.of(), + List.of(), + List.of( + new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T11:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "vu-1" + ), + new ExtractedCardVehicleUsageInterval( + "CVU-2", + OffsetDateTime.parse("2026-05-01T12:00:00Z"), + OffsetDateTime.parse("2026-05-01T13:00:00Z"), + 201L, + 260L, + "12:REG-2", + "VIN-2", + "vu-2" + ) + ), + List.of( + new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:30:00Z"), OffsetDateTime.parse("2026-05-01T09:00:00Z"), "WORK", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"), + new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-01T09:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b") + ), + List.of(), + List.of() + ); + TachographFileSession session = new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null), + Map.of(driver.driverKey(), driver), + new ExtractionStats(1, 2, 2, 1, 1, 0), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + repository.save(session); + + TachographEsperDriverProcessingResultDto result = + service.getEsperDriverProcessingResults(session.sessionId(), driver.driverKey()); + + assertThat(result.sourceKind()).isEqualTo("DRIVER_CARD"); + assertThat(result.activityIntervalCount()).isEqualTo(2); + assertThat(result.drivingIntervalCount()).isEqualTo(1); + assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2); + assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); + assertThat(result.vuCardAbsentIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:01Z")); + assertThat(result.vuCardAbsentIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:00:00Z")); + } + @Test void evaluatesOperatingPeriodsFromSessionTimeline() { EventHubProperties properties = new EventHubProperties();