Add tachograph Esper driver event projections

This commit is contained in:
trifonovt 2026-05-13 11:32:16 +02:00
parent 9e6f8efb26
commit 0e2b83270c
16 changed files with 1093 additions and 2 deletions

View File

@ -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", "name": "Process tachograph file session operating periods",
"request": { "request": {

View File

@ -1,6 +1,7 @@
package at.procon.eventhub.tachographfilesession.api; package at.procon.eventhub.tachographfilesession.api;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse; 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.TachographFileDriverDetailDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
@ -66,6 +67,14 @@ public class TachographFileSessionController {
return ResponseEntity.ok(service.getDriver(sessionId, driverKey)); return ResponseEntity.ok(service.getDriver(sessionId, driverKey));
} }
@GetMapping("/{sessionId}/drivers/{driverKey}/processing/esper-events")
public ResponseEntity<TachographEsperDriverProcessingResultDto> getEsperDriverProcessingResults(
@PathVariable UUID sessionId,
@PathVariable String driverKey
) {
return ResponseEntity.ok(processingService.getEsperDriverProcessingResults(sessionId, driverKey));
}
@PostMapping("/{sessionId}/drivers/{driverKey}/processing/operating-periods") @PostMapping("/{sessionId}/drivers/{driverKey}/processing/operating-periods")
public ResponseEntity<TachographOperatingPeriodsProcessingResultDto> evaluateOperatingPeriods( public ResponseEntity<TachographOperatingPeriodsProcessingResultDto> evaluateOperatingPeriods(
@PathVariable UUID sessionId, @PathVariable UUID sessionId,

View File

@ -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<TachographEsperActivityIntervalEvent> activityIntervals,
List<TachographEsperActivityIntervalEvent> drivingIntervals,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
List<String> notes
) {
}

View File

@ -3,8 +3,11 @@ package at.procon.eventhub.tachographfilesession.model;
import java.time.Duration; import java.time.Duration;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.UUID;
public record ResolvedVehicleUsageInterval( public record ResolvedVehicleUsageInterval(
UUID sessionId,
String driverKey,
String intervalId, String intervalId,
OffsetDateTime from, OffsetDateTime from,
OffsetDateTime to, OffsetDateTime to,
@ -17,6 +20,8 @@ public record ResolvedVehicleUsageInterval(
List<String> sourceIntervalIds List<String> sourceIntervalIds
) { ) {
public static ResolvedVehicleUsageInterval resolved( public static ResolvedVehicleUsageInterval resolved(
UUID sessionId,
String driverKey,
String intervalId, String intervalId,
OffsetDateTime from, OffsetDateTime from,
OffsetDateTime to, OffsetDateTime to,
@ -28,6 +33,8 @@ public record ResolvedVehicleUsageInterval(
List<String> sourceIntervalIds List<String> sourceIntervalIds
) { ) {
return new ResolvedVehicleUsageInterval( return new ResolvedVehicleUsageInterval(
sessionId,
driverKey,
intervalId, intervalId,
from, from,
to, to,

View File

@ -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<String> sourceIntervalIds,
boolean synthetic,
boolean clippedToRequestedPeriod,
String level
) {
}

View File

@ -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<String> sourceIntervalIds
) {
}

View File

@ -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
) {
}

View File

@ -1,5 +1,15 @@
package at.procon.eventhub.tachographfilesession.service; 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.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval; import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval; 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.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; 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 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.time.OffsetDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; 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.stereotype.Component;
import org.springframework.util.StreamUtils;
@Component @Component
public class DriverTimelineBuilder { 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) { public ResolvedDriverTimeline build(TachographFileSession session, DriverExtractionSession driverSession) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT"; String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(driverSession.cardVehicleUsageIntervals(), sourceKind); List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(
session.sessionId(),
driverSession.driverKey(),
driverSession.cardVehicleUsageIntervals(),
sourceKind
);
List<ResolvedActivityInterval> activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind); List<ResolvedActivityInterval> activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind);
List<ExtractedSupportEvent> supportEvents = driverSession.supportEvents().stream() List<ExtractedSupportEvent> supportEvents = driverSession.supportEvents().stream()
.sorted(Comparator.comparing(ExtractedSupportEvent::occurredAt) .sorted(Comparator.comparing(ExtractedSupportEvent::occurredAt)
@ -44,7 +80,200 @@ public class DriverTimelineBuilder {
); );
} }
public List<TachographEsperActivityIntervalEvent> buildEsperActivityIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedActivityInterval> activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind);
return buildEsperActivityIntervalEvents(session.sessionId(), driverSession.driverKey(), activityIntervals);
}
public List<TachographEsperActivityIntervalEvent> buildEsperActivityIntervalEvents(
UUID sessionId,
String driverKey,
ResolvedDriverTimeline timeline
) {
return timeline == null
? List.of()
: buildEsperActivityIntervalEvents(sessionId, driverKey, timeline.activityIntervals());
}
public List<TachographEsperActivityIntervalEvent> buildEsperDrivingIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedActivityInterval> activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind);
return buildEsperDrivingIntervalEvents(session.sessionId(), driverSession.driverKey(), activityIntervals);
}
public List<TachographEsperActivityIntervalEvent> buildEsperDrivingIntervalEvents(
UUID sessionId,
String driverKey,
ResolvedDriverTimeline timeline
) {
if (timeline == null) {
return List.of();
}
return buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline.activityIntervals());
}
public List<TachographEsperVehicleUsageIntervalEvent> buildEsperVehicleUsageIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(
session.sessionId(),
driverSession.driverKey(),
driverSession.cardVehicleUsageIntervals(),
sourceKind
);
return buildEsperVehicleUsageIntervalEvents(vehicleUsageIntervals);
}
public List<TachographEsperVehicleUsageIntervalEvent> buildEsperVehicleUsageIntervalEvents(
ResolvedDriverTimeline timeline
) {
return timeline == null ? List.of() : buildEsperVehicleUsageIntervalEvents(timeline.vehicleUsageIntervals());
}
public List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(
session.sessionId(),
driverSession.driverKey(),
driverSession.cardVehicleUsageIntervals(),
sourceKind
);
return buildEsperVuCardAbsentIntervalEvents(vehicleUsageIntervals);
}
public List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents(
ResolvedDriverTimeline timeline
) {
return timeline == null ? List.of() : buildEsperVuCardAbsentIntervalEvents(timeline.vehicleUsageIntervals());
}
private List<TachographEsperActivityIntervalEvent> buildEsperActivityIntervalEvents(
UUID sessionId,
String driverKey,
List<ResolvedActivityInterval> activityIntervals
) {
if (activityIntervals == null || activityIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperActivityIntervalEvent> 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<TachographEsperActivityIntervalEvent> buildEsperDrivingIntervalEvents(
UUID sessionId,
String driverKey,
List<ResolvedActivityInterval> activityIntervals
) {
if (activityIntervals == null || activityIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperActivityIntervalEvent> 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<TachographEsperVehicleUsageIntervalEvent> buildEsperVehicleUsageIntervalEvents(
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals
) {
if (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperVehicleUsageIntervalEvent> 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<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents(
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals
) {
if (vehicleUsageIntervals == null || vehicleUsageIntervals.size() < 2) {
return List.of();
}
List<TachographEsperVuCardAbsentIntervalEvent> 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<ResolvedVehicleUsageInterval> mergeVehicleUsageIntervals( private List<ResolvedVehicleUsageInterval> mergeVehicleUsageIntervals(
UUID sessionId,
String driverKey,
List<ExtractedCardVehicleUsageInterval> rawIntervals, List<ExtractedCardVehicleUsageInterval> rawIntervals,
String sourceKind String sourceKind
) { ) {
@ -54,6 +283,8 @@ public class DriverTimelineBuilder {
List<ResolvedVehicleUsageInterval> sorted = rawIntervals.stream() List<ResolvedVehicleUsageInterval> sorted = rawIntervals.stream()
.filter(interval -> interval.from() != null && (interval.to() == null || interval.to().isAfter(interval.from()))) .filter(interval -> interval.from() != null && (interval.to() == null || interval.to().isAfter(interval.from())))
.map(interval -> ResolvedVehicleUsageInterval.resolved( .map(interval -> ResolvedVehicleUsageInterval.resolved(
sessionId,
driverKey,
interval.intervalId(), interval.intervalId(),
interval.from(), interval.from(),
interval.to(), interval.to(),
@ -79,6 +310,8 @@ public class DriverTimelineBuilder {
if (canMerge(current, next)) { if (canMerge(current, next)) {
currentSources.addAll(next.sourceIntervalIds()); currentSources.addAll(next.sourceIntervalIds());
current = ResolvedVehicleUsageInterval.resolved( current = ResolvedVehicleUsageInterval.resolved(
current.sessionId(),
current.driverKey(),
current.intervalId() + "+" + next.intervalId(), current.intervalId() + "+" + next.intervalId(),
current.from(), current.from(),
mergedTo(current.to(), next.to()), mergedTo(current.to(), next.to()),
@ -220,4 +453,210 @@ public class DriverTimelineBuilder {
private OffsetDateTime mergeBoundary(OffsetDateTime endInclusive) { private OffsetDateTime mergeBoundary(OffsetDateTime endInclusive) {
return endInclusive == null ? OffsetDateTime.MAX : endInclusive.plusSeconds(1); return endInclusive == null ? OffsetDateTime.MAX : endInclusive.plusSeconds(1);
} }
private void executeWithRuntime(
Consumer<Configuration> configurationSetup,
String epl,
String statementName,
Consumer<EventBean[]> listener,
Consumer<EPRuntime> 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<String, Object> activityIntervalInputDefinition() {
Map<String, Object> 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<String, Object> vehicleUsageIntervalInputDefinition() {
Map<String, Object> 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<String, Object> toActivityIntervalInputMap(
UUID sessionId,
String driverKey,
ResolvedActivityInterval interval
) {
Map<String, Object> 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<String, Object> toVehicleUsageIntervalInputMap(ResolvedVehicleUsageInterval interval) {
Map<String, Object> 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<TachographEsperActivityIntervalEvent> 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<TachographEsperVehicleUsageIntervalEvent> 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<TachographEsperVuCardAbsentIntervalEvent> 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<String> castSourceIntervalIds(Object value) {
return value == null ? List.of() : List.copyOf((List<String>) 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);
}
}
} }

View File

@ -1,6 +1,7 @@
package at.procon.eventhub.tachographfilesession.service; package at.procon.eventhub.tachographfilesession.service;
import at.procon.eventhub.config.EventHubProperties; 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.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; 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.ProcessedShiftDrivingEvaluation;
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; 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.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.Duration; import java.time.Duration;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.ArrayList; 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<TachographEsperActivityIntervalEvent> activityIntervals =
driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline);
List<TachographEsperActivityIntervalEvent> drivingIntervals =
driverTimelineBuilder.buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline);
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals =
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline);
List<TachographEsperVuCardAbsentIntervalEvent> 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<ResolvedActivityInterval> synthesizeUnknownGaps( private List<ResolvedActivityInterval> synthesizeUnknownGaps(
List<ResolvedActivityInterval> knownIntervals, List<ResolvedActivityInterval> knownIntervals,
Duration gapDetectionTolerance Duration gapDetectionTolerance
@ -660,6 +703,14 @@ public class TachographFileSessionProcessingService {
); );
} }
private List<String> 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) { private OffsetDateTime utc(OffsetDateTime value) {
return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC); return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC);
} }

View File

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

View File

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

View File

@ -0,0 +1,15 @@
@name('vehicleUsageIntervals')
select
sessionId,
driverKey,
intervalId,
startedAt,
endedAt,
durationSeconds,
odometerBeginKm,
odometerEndKm,
registrationKey,
vehicleKey,
sourceKind,
sourceIntervalIds
from TachographVehicleUsageIntervalInputEvent

View File

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

View File

@ -9,6 +9,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse; 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.TachographFileDriverDetailDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; 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.TachographFileSessionListDriversResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto; import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats; 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.TachographFileSessionProcessingService;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionService; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionService;
import java.time.OffsetDateTime;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -58,6 +63,84 @@ class TachographFileSessionControllerTest {
when(service.getSession(sessionId)).thenReturn(summary); when(service.getSession(sessionId)).thenReturn(summary);
when(service.listDrivers(sessionId)).thenReturn(new TachographFileSessionListDriversResponse(sessionId, List.of(driver))); 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(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))) when(processingService.evaluateOperatingPeriods(eq(sessionId), eq("12:123"), org.mockito.ArgumentMatchers.any(TachographOperatingPeriodsProcessingRequest.class)))
.thenReturn(new TachographOperatingPeriodsProcessingResultDto( .thenReturn(new TachographOperatingPeriodsProcessingResultDto(
sessionId, sessionId,
@ -104,6 +187,14 @@ class TachographFileSessionControllerTest {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.driverKey").value("12:123")); .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") mockMvc.perform(post("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/operating-periods", sessionId, "12:123")
.contentType("application/json") .contentType("application/json")
.content(""" .content("""

View File

@ -9,6 +9,9 @@ import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats; import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; 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.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata; import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
import java.time.Instant; import java.time.Instant;
@ -100,6 +103,8 @@ class DriverTimelineBuilderTest {
assertThat(timeline.sourceKind()).isEqualTo("DRIVER_CARD"); assertThat(timeline.sourceKind()).isEqualTo("DRIVER_CARD");
assertThat(timeline.vehicleUsageIntervals()).hasSize(1); 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).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.vehicleUsageIntervals().get(0).to()).isEqualTo(OffsetDateTime.parse("2026-05-02T08:00:00Z"));
assertThat(timeline.activityIntervals()).hasSize(1); assertThat(timeline.activityIntervals()).hasSize(1);
@ -169,9 +174,205 @@ class DriverTimelineBuilderTest {
ResolvedDriverTimeline timeline = builder.build(session, driver); ResolvedDriverTimeline timeline = builder.build(session, driver);
assertThat(timeline.vehicleUsageIntervals()).hasSize(1); 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).from()).isEqualTo(OffsetDateTime.parse("2026-05-01T00:00:00Z"));
assertThat(timeline.vehicleUsageIntervals().get(0).to()).isNull(); assertThat(timeline.vehicleUsageIntervals().get(0).to()).isNull();
assertThat(timeline.vehicleUsageIntervals().get(0).sourceIntervalIds()).containsExactly("CVU-1", "CVU-2"); assertThat(timeline.vehicleUsageIntervals().get(0).sourceIntervalIds()).containsExactly("CVU-1", "CVU-2");
assertThat(timeline.loadedTo()).isEqualTo(OffsetDateTime.parse("2026-05-02T09:00:00Z")); 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<TachographEsperActivityIntervalEvent> activityEvents =
builder.buildEsperActivityIntervalEvents(session, driver);
List<TachographEsperActivityIntervalEvent> 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<TachographEsperVehicleUsageIntervalEvent> 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<TachographEsperVuCardAbsentIntervalEvent> 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");
}
} }

View File

@ -3,6 +3,7 @@ package at.procon.eventhub.tachographfilesession.service;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import at.procon.eventhub.config.EventHubProperties; 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.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
@ -21,6 +22,74 @@ import org.junit.jupiter.api.Test;
class TachographFileSessionProcessingServiceTest { 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 @Test
void evaluatesOperatingPeriodsFromSessionTimeline() { void evaluatesOperatingPeriodsFromSessionTimeline() {
EventHubProperties properties = new EventHubProperties(); EventHubProperties properties = new EventHubProperties();