Add runtime processing API and rest coverage projections

This commit is contained in:
trifonovt 2026-05-21 11:49:20 +02:00
parent 1ef4ba96bd
commit 9b86d13001
17 changed files with 1482 additions and 56 deletions

View File

@ -458,6 +458,179 @@
}
}
]
},
{
"name": "Runtime processing",
"item": [
{
"name": "Load runtime driver events from tachograph file session",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_FILE_SESSION\"],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 0\n}"
},
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-events"
]
}
}
},
{
"name": "Load runtime driver timeline from tachograph file session",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_FILE_SESSION\"],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 0\n}"
},
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-timeline"
]
}
}
},
{
"name": "Load runtime driver events from source DBs",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_DB\", \"YELLOWFOX_DB\"],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{runtimeDriverSourceEntityId}}\",\n \"driverCardNation\": \"{{runtimeDriverCardNation}}\",\n \"driverCardNumber\": \"{{runtimeDriverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 30\n}"
},
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-events"
]
}
}
},
{
"name": "Load runtime driver timeline from source DBs",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_DB\", \"YELLOWFOX_DB\"],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{runtimeDriverSourceEntityId}}\",\n \"driverCardNation\": \"{{runtimeDriverCardNation}}\",\n \"driverCardNumber\": \"{{runtimeDriverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 30\n}"
},
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-timeline"
]
}
}
},
{
"name": "Load runtime driver events from local EventHub DB",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_DB\", \"YELLOWFOX_DB\"],\n \"eventBackend\": \"EVENTHUB_DB\",\n \"driverSourceEntityId\": \"{{runtimeDriverSourceEntityId}}\",\n \"driverCardNation\": \"{{runtimeDriverCardNation}}\",\n \"driverCardNumber\": \"{{runtimeDriverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 30\n}"
},
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-events"
]
}
}
},
{
"name": "Load runtime driver timeline from local EventHub DB",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_DB\", \"YELLOWFOX_DB\"],\n \"eventBackend\": \"EVENTHUB_DB\",\n \"driverSourceEntityId\": \"{{runtimeDriverSourceEntityId}}\",\n \"driverCardNation\": \"{{runtimeDriverCardNation}}\",\n \"driverCardNumber\": \"{{runtimeDriverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 30\n}"
},
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-timeline"
]
}
}
}
]
}
],
"variable": [
@ -501,6 +674,18 @@
"key": "driverKey",
"value": "12:12345678901200"
},
{
"key": "runtimeDriverSourceEntityId",
"value": "DRIVER:42"
},
{
"key": "runtimeDriverCardNation",
"value": "12"
},
{
"key": "runtimeDriverCardNumber",
"value": "12345678901200"
},
{
"key": "tachographDddFile",
"value": "C:\\\\temp\\\\driver-card.ddd"

View File

@ -0,0 +1,42 @@
package at.procon.eventhub.processing.api;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService;
import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/eventhub/runtime-processing")
public class UnifiedRuntimeProcessingController {
private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
private final UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService;
public UnifiedRuntimeProcessingController(
UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService
) {
this.eventAssemblyService = eventAssemblyService;
this.runtimeDriverTimelineService = runtimeDriverTimelineService;
}
@PostMapping("/driver-events")
public ResponseEntity<UnifiedRuntimeEventBundle> loadDriverEvents(
@RequestBody UnifiedRuntimeProcessingApiRequest request
) {
return ResponseEntity.ok(eventAssemblyService.assembleDriverScopedEvents(request.toRuntimeRequest()));
}
@PostMapping("/driver-timeline")
public ResponseEntity<ResolvedDriverTimeline> loadDriverTimeline(
@RequestBody UnifiedRuntimeProcessingApiRequest request
) {
return ResponseEntity.ok(runtimeDriverTimelineService.loadDriverTimeline(request.toRuntimeRequest()));
}
}

View File

@ -0,0 +1,36 @@
package at.procon.eventhub.processing.api;
import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInSessionException;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
import java.time.OffsetDateTime;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice(basePackageClasses = UnifiedRuntimeProcessingController.class)
public class UnifiedRuntimeProcessingExceptionHandler {
@ExceptionHandler({
TachographFileSessionNotFoundException.class,
DriverNotFoundInSessionException.class
})
public ResponseEntity<Map<String, Object>> notFound(RuntimeException exception) {
return error(HttpStatus.NOT_FOUND, exception);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, Object>> badRequest(RuntimeException exception) {
return error(HttpStatus.BAD_REQUEST, exception);
}
private ResponseEntity<Map<String, Object>> error(HttpStatus status, RuntimeException exception) {
return ResponseEntity.status(status).body(Map.of(
"timestamp", OffsetDateTime.now().toString(),
"status", status.value(),
"error", status.getReasonPhrase(),
"message", exception.getMessage()
));
}
}

View File

@ -0,0 +1,40 @@
package at.procon.eventhub.processing.dto;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import java.time.OffsetDateTime;
import java.util.Set;
import java.util.UUID;
public record UnifiedRuntimeProcessingApiRequest(
UUID sessionId,
String tenantKey,
Set<UnifiedEventSourceFamily> sourceFamilies,
UnifiedRuntimeEventBackend eventBackend,
String driverKey,
String driverSourceEntityId,
String driverCardNation,
String driverCardNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
Boolean expandVehicleEvents,
Integer vehicleExpansionPaddingMinutes
) {
public UnifiedRuntimeProcessingRequest toRuntimeRequest() {
return new UnifiedRuntimeProcessingRequest(
sessionId,
tenantKey,
sourceFamilies,
eventBackend,
driverKey,
driverSourceEntityId,
driverCardNation,
driverCardNumber,
occurredFrom,
occurredTo,
expandVehicleEvents == null || expandVehicleEvents,
vehicleExpansionPaddingMinutes == null ? 0 : Math.max(0, vehicleExpansionPaddingMinutes)
);
}
}

View File

@ -1,8 +1,10 @@
package at.procon.eventhub.tachographfilesession.dto;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.OffsetDateTime;
@ -22,7 +24,10 @@ public record TachographEsperDriverProcessingResultDto(
int drivingInterruptionIntervalCount,
int drivingInterruptionVehicleChangeIntervalCount,
int dailyWeeklyRestCandidateIntervalCount,
int dailyWeeklyRestCandidateCoverageIntervalCount,
int unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
int potentialHomeOvernightStayIntervalCount,
int potentialInVehicleOvernightStayIntervalCount,
int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount,
List<TachographEsperActivityIntervalEvent> activityIntervals,
@ -30,7 +35,10 @@ public record TachographEsperDriverProcessingResultDto(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals,
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals,
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals,
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
List<String> notes

View File

@ -0,0 +1,23 @@
package at.procon.eventhub.tachographfilesession.model;
import java.time.OffsetDateTime;
import java.util.UUID;
public record TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
UUID sessionId,
String driverKey,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long durationSeconds,
long cardPresentDurationSeconds,
double cardPresentCoveragePercent,
long unknownDurationSeconds,
double unknownCoveragePercent,
String previousDrivingSourceIntervalId,
String nextDrivingSourceIntervalId,
String previousRegistrationKey,
String nextRegistrationKey,
String previousVehicleKey,
String nextVehicleKey
) {
}

View File

@ -5,8 +5,11 @@ import java.util.List;
public record TachographEsperDrivingDerivedProjectionBundle(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals,
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals,
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals,
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals
) {
}

View File

@ -0,0 +1,21 @@
package at.procon.eventhub.tachographfilesession.model;
import java.time.OffsetDateTime;
import java.util.UUID;
public record TachographEsperPotentialInVehicleOvernightStayIntervalEvent(
UUID sessionId,
String driverKey,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long durationSeconds,
long cardPresentDurationSeconds,
double cardPresentCoveragePercent,
String previousDrivingSourceIntervalId,
String nextDrivingSourceIntervalId,
String previousRegistrationKey,
String nextRegistrationKey,
String previousVehicleKey,
String nextVehicleKey
) {
}

View File

@ -14,9 +14,11 @@ import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
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.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import java.io.IOException;
@ -100,9 +102,12 @@ public class DriverTimelineReusableProjectionBuilder {
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals = new ArrayList<>();
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals = new ArrayList<>();
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals = new ArrayList<>();
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals = new ArrayList<>();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals = new ArrayList<>();
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = new ArrayList<>();
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals = new ArrayList<>();
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals = new ArrayList<>();
executeWithRuntime(
configuration -> {
@ -119,9 +124,12 @@ public class DriverTimelineReusableProjectionBuilder {
Map.of(
"drivingInterruptionIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionIntervals),
"dailyWeeklyRestCandidateIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, dailyWeeklyRestCandidateIntervals),
"dailyWeeklyRestCandidateCoverageIntervals", newData -> collectDailyWeeklyRestCandidateCoverageIntervalEvents(newData, dailyWeeklyRestCandidateCoverageIntervals),
"unclassifiedDailyWeeklyRestCandidateCoverageIntervals", newData -> collectDailyWeeklyRestCandidateCoverageIntervalEvents(newData, unclassifiedDailyWeeklyRestCandidateCoverageIntervals),
"drivingInterruptionVehicleChangeIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionVehicleChangeIntervals),
"vuCardAbsentIntervals", newData -> collectVuCardAbsentIntervalEvents(newData, vuCardAbsentIntervals),
"potentialHomeOvernightStayIntervals", newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, potentialHomeOvernightStayIntervals)
"potentialHomeOvernightStayIntervals", newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, potentialHomeOvernightStayIntervals),
"potentialInVehicleOvernightStayIntervals", newData -> collectPotentialInVehicleOvernightStayIntervalEvents(newData, potentialInVehicleOvernightStayIntervals)
),
runtime -> {
if (vehicleUsageIntervals != null) {
@ -146,14 +154,20 @@ public class DriverTimelineReusableProjectionBuilder {
return new TachographEsperDrivingDerivedProjectionBundle(
sortDrivingInterruptionIntervals(drivingInterruptionIntervals),
sortDrivingInterruptionIntervals(dailyWeeklyRestCandidateIntervals),
sortDailyWeeklyRestCandidateCoverageIntervals(dailyWeeklyRestCandidateCoverageIntervals),
sortDailyWeeklyRestCandidateCoverageIntervals(unclassifiedDailyWeeklyRestCandidateCoverageIntervals),
sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals),
sortVuCardAbsentIntervals(vuCardAbsentIntervals),
sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals)
sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals),
sortPotentialInVehicleOvernightStayIntervals(potentialInVehicleOvernightStayIntervals)
);
}
private TachographEsperDrivingDerivedProjectionBundle emptyBundle() {
return new TachographEsperDrivingDerivedProjectionBundle(
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
@ -364,6 +378,36 @@ public class DriverTimelineReusableProjectionBuilder {
}
}
private void collectDailyWeeklyRestCandidateCoverageIntervalEvents(
EventBean[] newData,
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> target
) {
if (newData == null) {
return;
}
for (EventBean event : newData) {
long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
target.add(new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
(UUID) event.get("sessionId"),
(String) event.get("driverKey"),
OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC),
OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC),
(Long) event.get("durationSeconds"),
(Long) event.get("cardPresentDurationSeconds"),
(Double) event.get("cardPresentCoveragePercent"),
(Long) event.get("unknownDurationSeconds"),
(Double) event.get("unknownCoveragePercent"),
(String) event.get("previousDrivingSourceIntervalId"),
(String) event.get("nextDrivingSourceIntervalId"),
(String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey")
));
}
}
private void collectPotentialHomeOvernightStayIntervalEvents(
EventBean[] newData,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> target
@ -392,6 +436,34 @@ public class DriverTimelineReusableProjectionBuilder {
}
}
private void collectPotentialInVehicleOvernightStayIntervalEvents(
EventBean[] newData,
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> target
) {
if (newData == null) {
return;
}
for (EventBean event : newData) {
long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
target.add(new TachographEsperPotentialInVehicleOvernightStayIntervalEvent(
(UUID) event.get("sessionId"),
(String) event.get("driverKey"),
OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC),
OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC),
(Long) event.get("durationSeconds"),
(Long) event.get("cardPresentDurationSeconds"),
(Double) event.get("cardPresentCoveragePercent"),
(String) event.get("previousDrivingSourceIntervalId"),
(String) event.get("nextDrivingSourceIntervalId"),
(String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey")
));
}
}
private List<TachographEsperDrivingInterruptionIntervalEvent> sortDrivingInterruptionIntervals(
List<TachographEsperDrivingInterruptionIntervalEvent> intervals
) {
@ -410,6 +482,15 @@ public class DriverTimelineReusableProjectionBuilder {
.toList();
}
private List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> sortDailyWeeklyRestCandidateCoverageIntervals(
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> intervals
) {
return intervals.stream()
.sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt)
.thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> sortPotentialHomeOvernightStayIntervals(
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals
) {
@ -419,6 +500,15 @@ public class DriverTimelineReusableProjectionBuilder {
.toList();
}
private List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> sortPotentialInVehicleOvernightStayIntervals(
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> intervals
) {
return intervals.stream()
.sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt)
.thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt))
.toList();
}
private String renderDrivingDerivedProjectionBundleEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) {
return DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE
.replace(

View File

@ -111,7 +111,9 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
eventSource,
sourcePackageRef
);
List<EventHubEventDto> supportEvents = buildSupportEvents(
List<EventHubEventDto> supportEvents = List.of();
/*
buildSupportEvents(
session,
timeline.supportEvents(),
driverRef,
@ -120,6 +122,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
eventSource,
sourcePackageRef
);
*/
return new TachographTimelineEventBundle(activityEvents, vehicleUsageEvents, supportEvents);
}

View File

@ -13,10 +13,12 @@ import at.procon.eventhub.tachographfilesession.model.ProcessedShiftDrivingEvalu
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.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.Duration;
@ -209,8 +211,33 @@ public class TachographFileSessionProcessingService {
requestedFrom,
requestedTo
);
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals =
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline);
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals =
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals =
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
derivedProjectionBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals =
clipEsperPotentialInVehicleOvernightStayIntervalEvents(
derivedProjectionBundle.potentialInVehicleOvernightStayIntervals(),
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents(
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline),
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
@ -233,7 +260,10 @@ public class TachographFileSessionProcessingService {
drivingInterruptionIntervals.size(),
drivingInterruptionVehicleChangeIntervals.size(),
dailyWeeklyRestCandidateIntervals.size(),
dailyWeeklyRestCandidateCoverageIntervals.size(),
unclassifiedDailyWeeklyRestCandidateCoverageIntervals.size(),
potentialHomeOvernightStayIntervals.size(),
potentialInVehicleOvernightStayIntervals.size(),
vehicleUsageIntervals.size(),
vuCardAbsentIntervals.size(),
activityIntervals,
@ -241,7 +271,10 @@ public class TachographFileSessionProcessingService {
drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals,
dailyWeeklyRestCandidateIntervals,
dailyWeeklyRestCandidateCoverageIntervals,
unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
potentialHomeOvernightStayIntervals,
potentialInVehicleOvernightStayIntervals,
vehicleUsageIntervals,
vuCardAbsentIntervals,
esperProjectionNotes()
@ -413,6 +446,67 @@ public class TachographFileSessionProcessingService {
.toList();
}
private List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
long durationSeconds = Duration.between(start, end).getSeconds();
long cardPresentDurationSeconds = overlapVehicleUsageSeconds(
start,
end,
rawVehicleUsageIntervals,
interval.driverKey(),
null
);
double cardPresentCoveragePercent = durationSeconds == 0L
? 0.0d
: (cardPresentDurationSeconds * 100.0d) / durationSeconds;
long unknownDurationSeconds = overlapSeconds(
start,
end,
rawVuCardAbsentIntervals,
interval.driverKey()
);
double unknownCoveragePercent = durationSeconds == 0L
? 0.0d
: (unknownDurationSeconds * 100.0d) / durationSeconds;
return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
durationSeconds,
cardPresentDurationSeconds,
cardPresentCoveragePercent,
unknownDurationSeconds,
unknownCoveragePercent,
interval.previousDrivingSourceIntervalId(),
interval.nextDrivingSourceIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt)
.thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> clipEsperPotentialHomeOvernightStayIntervalEvents(
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
@ -461,6 +555,55 @@ public class TachographFileSessionProcessingService {
.toList();
}
private List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> clipEsperPotentialInVehicleOvernightStayIntervalEvents(
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> intervals,
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
long durationSeconds = Duration.between(start, end).getSeconds();
long cardPresentDurationSeconds = overlapVehicleUsageSeconds(
start,
end,
rawVehicleUsageIntervals,
interval.driverKey(),
interval.previousRegistrationKey()
);
double cardPresentCoveragePercent = durationSeconds == 0L
? 0.0d
: (cardPresentDurationSeconds * 100.0d) / durationSeconds;
return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
durationSeconds,
cardPresentDurationSeconds,
cardPresentCoveragePercent,
interval.previousDrivingSourceIntervalId(),
interval.nextDrivingSourceIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt)
.thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt))
.toList();
}
private List<ResolvedActivityInterval> synthesizeUnknownGaps(
List<ResolvedActivityInterval> knownIntervals,
Duration gapDetectionTolerance
@ -1027,7 +1170,10 @@ public class TachographFileSessionProcessingService {
"Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.",
"Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.",
"Daily/weekly rest candidate intervals are driving interruption intervals longer than the configured minimum rest-period threshold.",
"Potential home overnight stay intervals are vehicle-change daily/weekly rest candidates where VU card-absent overlap covers at least 95% of the candidate interval.",
"Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and unknown-coverage metrics computed from vehicle-usage and VU card-absent overlap.",
"Unclassified daily/weekly rest candidate coverage intervals are the rest candidates that are neither potential home overnight stays nor potential in-vehicle overnight stays.",
"Potential home overnight stay intervals are vehicle-change daily/weekly rest candidate coverage intervals where VU card-absent overlap covers at least 95% of the candidate interval.",
"Potential in-vehicle overnight stay intervals are no-change daily/weekly rest candidate coverage intervals where card-present overlap covers the candidate rest period.",
"VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver.",
"occurredFrom and occurredTo clip the returned interval projections to the requested UTC time window.",
"Vehicle-usage intervals clear clipped odometer endpoints because boundary odometer values cannot be recomputed safely from the source interval."
@ -1057,6 +1203,36 @@ public class TachographFileSessionProcessingService {
return total;
}
private long overlapVehicleUsageSeconds(
OffsetDateTime intervalStart,
OffsetDateTime intervalEnd,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
String driverKey,
String registrationKey
) {
if (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty()) {
return 0L;
}
long total = 0L;
for (TachographEsperVehicleUsageIntervalEvent vehicleUsage : vehicleUsageIntervals) {
if (!Objects.equals(driverKey, vehicleUsage.driverKey())) {
continue;
}
if (registrationKey != null && !Objects.equals(registrationKey, vehicleUsage.registrationKey())) {
continue;
}
if (vehicleUsage.endedAt() == null) {
continue;
}
OffsetDateTime overlapStart = max(intervalStart, vehicleUsage.startedAt());
OffsetDateTime overlapEnd = min(intervalEnd, vehicleUsage.endedAt());
if (overlapEnd.isAfter(overlapStart)) {
total += Duration.between(overlapStart, overlapEnd).getSeconds();
}
}
return total;
}
private OffsetDateTime utc(OffsetDateTime value) {
return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC);
}

View File

@ -18,12 +18,16 @@ import java.util.Comparator;
import java.util.HexFormat;
import java.util.List;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Service
public class TachographFileSessionService {
private static final Logger log = LoggerFactory.getLogger(TachographFileSessionService.class);
private final EventHubProperties properties;
private final TachographFileSessionRepository repository;
private final LegalRequirementsClient legalRequirementsClient;
@ -57,8 +61,14 @@ public class TachographFileSessionService {
validateFile(file);
byte[] fileBytes = file.getBytes();
validateFileBytes(fileBytes);
long uploadStartedAt = System.nanoTime();
LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadTachographFile(fileBytes, file.getOriginalFilename());
long uploadDurationMs = elapsedMillis(uploadStartedAt);
long parseStartedAt = System.nanoTime();
TachographXmlParser.ParsedTachographXml parsedXml = tachographXmlParser.parse(uploadResult.xmlContent());
long parseDurationMs = elapsedMillis(parseStartedAt);
Instant createdAt = Instant.now();
Instant expiresAt = createdAt.plus(properties.getTachographFileSession().getTtl());
boolean driverCardFile = "DriverCard".equals(parsedXml.rootElementName());
@ -74,9 +84,21 @@ public class TachographFileSessionService {
driverCardFile,
null
);
long extractStartedAt = System.nanoTime();
TachographFileSession session = driverCardFile
? driverCardExtractionService.extract(parsedXml, metadata, createdAt, expiresAt)
: vehicleUnitExtractionService.extract(parsedXml, metadata, createdAt, expiresAt);
long extractDurationMs = elapsedMillis(extractStartedAt);
log.info(
"Created tachograph file session timing: file='{}', type={}, uploadMs={}, parseMs={}, extractMs={}, sizeBytes={}",
file.getOriginalFilename(),
driverCardFile ? "DRIVER_CARD" : "VEHICLE_UNIT",
uploadDurationMs,
parseDurationMs,
extractDurationMs,
fileBytes.length
);
TachographFileSession saved = repository.save(session);
return new CreateTachographFileSessionResponse(toSummary(saved));
} catch (Exception e) {
@ -196,4 +218,8 @@ public class TachographFileSessionService {
throw new IllegalStateException("SHA-256 digest is not available.", e);
}
}
private long elapsedMillis(long startedAtNanos) {
return (System.nanoTime() - startedAtNanos) / 1_000_000L;
}
}

View File

@ -52,6 +52,69 @@ create schema DrivingInterruptionVehicleChangeInterval(
nextVehicleKey string
);
create schema DrivingInterruptionVehicleNotChangedInterval(
sessionId java.util.UUID,
driverKey string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
previousDrivingSourceIntervalId string,
nextDrivingSourceIntervalId string,
previousRegistrationKey string,
nextRegistrationKey string,
previousVehicleKey string,
nextVehicleKey string
);
create schema DailyWeeklyRestCandidateCoverageUnknownResolvedInterval(
sessionId java.util.UUID,
driverKey string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
unknownDurationSeconds long,
previousDrivingSourceIntervalId string,
nextDrivingSourceIntervalId string,
previousRegistrationKey string,
nextRegistrationKey string,
previousVehicleKey string,
nextVehicleKey string
);
create schema DailyWeeklyRestCandidateCoverageCardResolvedInterval(
sessionId java.util.UUID,
driverKey string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
cardPresentDurationSeconds long,
unknownDurationSeconds long,
previousDrivingSourceIntervalId string,
nextDrivingSourceIntervalId string,
previousRegistrationKey string,
nextRegistrationKey string,
previousVehicleKey string,
nextVehicleKey string
);
create schema DailyWeeklyRestCandidateCoverageInterval(
sessionId java.util.UUID,
driverKey string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
cardPresentDurationSeconds long,
cardPresentCoveragePercent double,
unknownDurationSeconds long,
unknownCoveragePercent double,
previousDrivingSourceIntervalId string,
nextDrivingSourceIntervalId string,
previousRegistrationKey string,
nextRegistrationKey string,
previousVehicleKey string,
nextVehicleKey string
);
create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent;
create schema VuCardAbsentInterval(
@ -84,6 +147,40 @@ create schema PotentialHomeOvernightStayInterval(
nextVehicleKey string
);
create schema PotentialInVehicleOvernightStayInterval(
sessionId java.util.UUID,
driverKey string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
cardPresentDurationSeconds long,
cardPresentCoveragePercent double,
previousDrivingSourceIntervalId string,
nextDrivingSourceIntervalId string,
previousRegistrationKey string,
nextRegistrationKey string,
previousVehicleKey string,
nextVehicleKey string
);
create schema UnclassifiedDailyWeeklyRestCandidateCoverageInterval(
sessionId java.util.UUID,
driverKey string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
cardPresentDurationSeconds long,
cardPresentCoveragePercent double,
unknownDurationSeconds long,
unknownCoveragePercent double,
previousDrivingSourceIntervalId string,
nextDrivingSourceIntervalId string,
previousRegistrationKey string,
nextRegistrationKey string,
previousVehicleKey string,
nextVehicleKey string
);
insert into SignificantDrivingInterval
select
sessionId,
@ -138,6 +235,164 @@ from DailyWeeklyRestCandidateInterval(
previousRegistrationKey != nextRegistrationKey
);
insert into DrivingInterruptionVehicleNotChangedInterval
select *
from DailyWeeklyRestCandidateInterval(
previousRegistrationKey is not null,
nextRegistrationKey is not null,
previousRegistrationKey = nextRegistrationKey
);
insert into DailyWeeklyRestCandidateCoverageUnknownResolvedInterval
select
c.sessionId as sessionId,
c.driverKey as driverKey,
c.startedAtEpochSecond as startedAtEpochSecond,
c.endedAtEpochSecond as endedAtEpochSecond,
c.durationSeconds as durationSeconds,
sum(
case
when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.durationSeconds
when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond
end
) as unknownDurationSeconds,
c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId,
c.previousRegistrationKey as previousRegistrationKey,
c.nextRegistrationKey as nextRegistrationKey,
c.previousVehicleKey as previousVehicleKey,
c.nextVehicleKey as nextVehicleKey
from DailyWeeklyRestCandidateInterval as c unidirectional,
VuCardAbsentInterval#keepall as u
where u.driverKey = c.driverKey
and u.startedAtEpochSecond < c.endedAtEpochSecond
and u.endedAtEpochSecond > c.startedAtEpochSecond
group by
c.sessionId,
c.driverKey,
c.startedAtEpochSecond,
c.endedAtEpochSecond,
c.durationSeconds,
c.previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId,
c.previousRegistrationKey,
c.nextRegistrationKey,
c.previousVehicleKey,
c.nextVehicleKey;
insert into DailyWeeklyRestCandidateCoverageUnknownResolvedInterval
select
c.sessionId as sessionId,
c.driverKey as driverKey,
c.startedAtEpochSecond as startedAtEpochSecond,
c.endedAtEpochSecond as endedAtEpochSecond,
c.durationSeconds as durationSeconds,
0L as unknownDurationSeconds,
c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId,
c.previousRegistrationKey as previousRegistrationKey,
c.nextRegistrationKey as nextRegistrationKey,
c.previousVehicleKey as previousVehicleKey,
c.nextVehicleKey as nextVehicleKey
from DailyWeeklyRestCandidateInterval as c
where not exists (
select * from VuCardAbsentInterval#keepall as u
where u.driverKey = c.driverKey
and u.startedAtEpochSecond < c.endedAtEpochSecond
and u.endedAtEpochSecond > c.startedAtEpochSecond
);
insert into DailyWeeklyRestCandidateCoverageCardResolvedInterval
select
c.sessionId as sessionId,
c.driverKey as driverKey,
c.startedAtEpochSecond as startedAtEpochSecond,
c.endedAtEpochSecond as endedAtEpochSecond,
c.durationSeconds as durationSeconds,
sum(
case
when v.startedAtEpochSecond <= c.startedAtEpochSecond and (v.endedAtEpochSecond is null or v.endedAtEpochSecond >= c.endedAtEpochSecond)
then c.durationSeconds
when v.startedAtEpochSecond <= c.startedAtEpochSecond
then v.endedAtEpochSecond - c.startedAtEpochSecond
when v.endedAtEpochSecond is null or v.endedAtEpochSecond >= c.endedAtEpochSecond
then c.endedAtEpochSecond - v.startedAtEpochSecond
else v.endedAtEpochSecond - v.startedAtEpochSecond
end
) as cardPresentDurationSeconds,
c.unknownDurationSeconds as unknownDurationSeconds,
c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId,
c.previousRegistrationKey as previousRegistrationKey,
c.nextRegistrationKey as nextRegistrationKey,
c.previousVehicleKey as previousVehicleKey,
c.nextVehicleKey as nextVehicleKey
from DailyWeeklyRestCandidateCoverageUnknownResolvedInterval as c unidirectional,
TachographVehicleUsageIntervalInputEvent#keepall as v
where v.driverKey = c.driverKey
and v.startedAtEpochSecond < c.endedAtEpochSecond
and (v.endedAtEpochSecond is null or v.endedAtEpochSecond > c.startedAtEpochSecond)
group by
c.sessionId,
c.driverKey,
c.startedAtEpochSecond,
c.endedAtEpochSecond,
c.durationSeconds,
c.unknownDurationSeconds,
c.previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId,
c.previousRegistrationKey,
c.nextRegistrationKey,
c.previousVehicleKey,
c.nextVehicleKey;
insert into DailyWeeklyRestCandidateCoverageCardResolvedInterval
select
c.sessionId as sessionId,
c.driverKey as driverKey,
c.startedAtEpochSecond as startedAtEpochSecond,
c.endedAtEpochSecond as endedAtEpochSecond,
c.durationSeconds as durationSeconds,
0L as cardPresentDurationSeconds,
c.unknownDurationSeconds as unknownDurationSeconds,
c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId,
c.previousRegistrationKey as previousRegistrationKey,
c.nextRegistrationKey as nextRegistrationKey,
c.previousVehicleKey as previousVehicleKey,
c.nextVehicleKey as nextVehicleKey
from DailyWeeklyRestCandidateCoverageUnknownResolvedInterval as c
where not exists (
select * from TachographVehicleUsageIntervalInputEvent#keepall as v
where v.driverKey = c.driverKey
and v.startedAtEpochSecond < c.endedAtEpochSecond
and (v.endedAtEpochSecond is null or v.endedAtEpochSecond > c.startedAtEpochSecond)
);
insert into DailyWeeklyRestCandidateCoverageInterval
select
sessionId,
driverKey,
startedAtEpochSecond,
endedAtEpochSecond,
durationSeconds,
cardPresentDurationSeconds,
(cardPresentDurationSeconds * 100.0d) / durationSeconds as cardPresentCoveragePercent,
unknownDurationSeconds,
(unknownDurationSeconds * 100.0d) / durationSeconds as unknownCoveragePercent,
previousDrivingSourceIntervalId,
nextDrivingSourceIntervalId,
previousRegistrationKey,
nextRegistrationKey,
previousVehicleKey,
nextVehicleKey
from DailyWeeklyRestCandidateCoverageCardResolvedInterval;
context PerDriver
create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent;
@ -180,62 +435,71 @@ select
c.startedAtEpochSecond as startedAtEpochSecond,
c.endedAtEpochSecond as endedAtEpochSecond,
c.durationSeconds as durationSeconds,
sum(
case
when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.durationSeconds
when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond
end
) as unknownDurationSeconds,
(sum(
case
when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.durationSeconds
when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond
end
) * 100.0d) / c.durationSeconds as unknownCoveragePercent,
c.unknownDurationSeconds as unknownDurationSeconds,
c.unknownCoveragePercent as unknownCoveragePercent,
c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId,
c.previousRegistrationKey as previousRegistrationKey,
c.nextRegistrationKey as nextRegistrationKey,
c.previousVehicleKey as previousVehicleKey,
c.nextVehicleKey as nextVehicleKey
from DrivingInterruptionVehicleChangeInterval as c unidirectional,
VuCardAbsentInterval#keepall as u
where u.driverKey = c.driverKey
and u.startedAtEpochSecond < c.endedAtEpochSecond
and u.endedAtEpochSecond > c.startedAtEpochSecond
group by
c.sessionId,
c.driverKey,
c.startedAtEpochSecond,
c.endedAtEpochSecond,
c.durationSeconds,
c.previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId,
c.previousRegistrationKey,
c.nextRegistrationKey,
c.previousVehicleKey,
c.nextVehicleKey
having sum(
case
when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.durationSeconds
when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond
end
) * 100L >= c.durationSeconds * 95L;
from DailyWeeklyRestCandidateCoverageInterval as c
where c.previousRegistrationKey is not null
and c.nextRegistrationKey is not null
and c.previousRegistrationKey != c.nextRegistrationKey
and c.unknownDurationSeconds * 100L >= c.durationSeconds * 95L;
insert into PotentialInVehicleOvernightStayInterval
select
c.sessionId as sessionId,
c.driverKey as driverKey,
c.startedAtEpochSecond as startedAtEpochSecond,
c.endedAtEpochSecond as endedAtEpochSecond,
c.durationSeconds as durationSeconds,
c.cardPresentDurationSeconds as cardPresentDurationSeconds,
c.cardPresentCoveragePercent as cardPresentCoveragePercent,
c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId,
c.previousRegistrationKey as previousRegistrationKey,
c.nextRegistrationKey as nextRegistrationKey,
c.previousVehicleKey as previousVehicleKey,
c.nextVehicleKey as nextVehicleKey
from DailyWeeklyRestCandidateCoverageInterval as c
where c.previousRegistrationKey is not null
and c.nextRegistrationKey is not null
and c.previousRegistrationKey = c.nextRegistrationKey
and c.cardPresentDurationSeconds >= c.durationSeconds;
insert into UnclassifiedDailyWeeklyRestCandidateCoverageInterval
select
c.sessionId as sessionId,
c.driverKey as driverKey,
c.startedAtEpochSecond as startedAtEpochSecond,
c.endedAtEpochSecond as endedAtEpochSecond,
c.durationSeconds as durationSeconds,
c.cardPresentDurationSeconds as cardPresentDurationSeconds,
c.cardPresentCoveragePercent as cardPresentCoveragePercent,
c.unknownDurationSeconds as unknownDurationSeconds,
c.unknownCoveragePercent as unknownCoveragePercent,
c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId,
c.previousRegistrationKey as previousRegistrationKey,
c.nextRegistrationKey as nextRegistrationKey,
c.previousVehicleKey as previousVehicleKey,
c.nextVehicleKey as nextVehicleKey
from DailyWeeklyRestCandidateCoverageInterval as c
where not (
c.previousRegistrationKey is not null
and c.nextRegistrationKey is not null
and c.previousRegistrationKey != c.nextRegistrationKey
and c.unknownDurationSeconds * 100L >= c.durationSeconds * 95L
)
and not (
c.previousRegistrationKey is not null
and c.nextRegistrationKey is not null
and c.previousRegistrationKey = c.nextRegistrationKey
and c.cardPresentDurationSeconds >= c.durationSeconds
);
@name('drivingInterruptionIntervals')
select * from DrivingInterruptionInterval;
@ -243,6 +507,9 @@ select * from DrivingInterruptionInterval;
@name('dailyWeeklyRestCandidateIntervals')
select * from DailyWeeklyRestCandidateInterval;
@name('dailyWeeklyRestCandidateCoverageIntervals')
select * from DailyWeeklyRestCandidateCoverageInterval;
@name('drivingInterruptionVehicleChangeIntervals')
select * from DrivingInterruptionVehicleChangeInterval;
@ -251,3 +518,9 @@ select * from VuCardAbsentInterval;
@name('potentialHomeOvernightStayIntervals')
select * from PotentialHomeOvernightStayInterval;
@name('potentialInVehicleOvernightStayIntervals')
select * from PotentialInVehicleOvernightStayInterval;
@name('unclassifiedDailyWeeklyRestCandidateCoverageIntervals')
select * from UnclassifiedDailyWeeklyRestCandidateCoverageInterval;

View File

@ -0,0 +1,192 @@
package at.procon.eventhub.processing.api;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventDomain;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.EventLifecycle;
import at.procon.eventhub.dto.EventType;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService;
import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService;
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
class UnifiedRuntimeProcessingControllerTest {
@Test
void loadsDriverEventsViaRuntimeApi() throws Exception {
UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);
UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService))
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
.build();
UUID sessionId = UUID.randomUUID();
UnifiedRuntimeProcessingRequest request = UnifiedRuntimeProcessingRequest.forTachographFileSession(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
true,
0
);
when(eventAssemblyService.assembleDriverScopedEvents(any()))
.thenReturn(new UnifiedRuntimeEventBundle(
request,
List.of(event("EV-1", "2026-05-01T08:00:00Z")),
List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")),
List.of(),
List.of(event("EV-1", "2026-05-01T08:00:00Z")),
List.of("note")
));
mockMvc.perform(post("/api/eventhub/runtime-processing/driver-events")
.contentType("application/json")
.content("""
{
"sessionId": "%s",
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION"],
"driverKey": "12:123",
"occurredFrom": "2026-05-01T08:00:00Z",
"occurredTo": "2026-05-01T10:00:00Z",
"expandVehicleEvents": true,
"vehicleExpansionPaddingMinutes": 0
}
""".formatted(sessionId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.request.sessionId").value(sessionId.toString()))
.andExpect(jsonPath("$.request.driverKey").value("12:123"))
.andExpect(jsonPath("$.discoveredVehicles[0].vin").value("VIN-1"))
.andExpect(jsonPath("$.mergedEvents[0].externalSourceEventId").value("EV-1"));
}
@Test
void loadsDriverTimelineViaRuntimeApi() throws Exception {
UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);
UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService))
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
.build();
when(timelineService.loadDriverTimeline(any()))
.thenReturn(new ResolvedDriverTimeline(
"UNIFIED_EVENT_STREAM",
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
List.of(ResolvedVehicleUsageInterval.resolved(
null,
"DRIVER:42",
"CVU-1",
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
100L,
200L,
"12:REG-1",
"VIN-1",
"DRIVER_CARD",
List.of("CVU-1")
)),
List.of(new ResolvedActivityInterval(
"ACT-1",
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
OffsetDateTime.parse("2026-05-01T09:00:00Z"),
1800L,
"DRIVE",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-1",
"VIN-1",
"DRIVER_CARD",
List.of("ACT-1"),
false,
false,
"RAW_INTERVAL"
)),
List.of(),
List.of()
));
mockMvc.perform(post("/api/eventhub/runtime-processing/driver-timeline")
.contentType("application/json")
.content("""
{
"tenantKey": "default",
"sourceFamilies": ["TACHOGRAPH_DB", "YELLOWFOX_DB"],
"eventBackend": "SOURCE_DB",
"driverSourceEntityId": "DRIVER:42",
"occurredFrom": "2026-05-01T08:00:00Z",
"occurredTo": "2026-05-01T10:00:00Z",
"expandVehicleEvents": false,
"vehicleExpansionPaddingMinutes": 15
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.sourceKind").value("UNIFIED_EVENT_STREAM"))
.andExpect(jsonPath("$.activityIntervals[0].intervalId").value("ACT-1"))
.andExpect(jsonPath("$.vehicleUsageIntervals[0].intervalId").value("CVU-1"));
}
@Test
void returnsBadRequestForInvalidRuntimeRequest() throws Exception {
UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);
UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService))
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
.build();
mockMvc.perform(post("/api/eventhub/runtime-processing/driver-events")
.contentType("application/json")
.content("""
{
"sourceFamilies": ["TACHOGRAPH_DB"],
"occurredFrom": "2026-05-01T08:00:00Z",
"occurredTo": "2026-05-01T10:00:00Z"
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").exists());
}
private EventHubEventDto event(String externalId, String occurredAt) {
return new EventHubEventDto(
UUID.randomUUID(),
externalId,
new DriverRefDto("12:123", null),
new VehicleRefDto("VEH-1", "VIN-1", "VR-1", new VehicleRegistrationRefDto("12", 12, "REG-1")),
OffsetDateTime.parse(occurredAt),
null,
OffsetDateTime.parse(occurredAt),
EventDomain.DRIVER_ACTIVITY,
EventType.DRIVE,
EventLifecycle.START,
null,
null,
null,
null,
null,
false,
null
);
}
}

View File

@ -20,6 +20,7 @@ import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDri
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.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
@ -85,6 +86,9 @@ class TachographFileSessionControllerTest {
1,
1,
1,
0,
1,
0,
2,
1,
List.of(new TachographEsperActivityIntervalEvent(
@ -164,6 +168,24 @@ class TachographFileSessionControllerTest {
"VIN-1",
"VIN-2"
)),
List.of(new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-12T10:00:00Z"),
OffsetDateTime.parse("2026-05-12T22:00:00Z"),
43_200L,
0L,
0.0d,
43_200L,
100.0d,
"ACT-2",
"ACT-3",
"12:REG-1",
"12:REG-2",
"VIN-1",
"VIN-2"
)),
List.of(),
List.of(new TachographEsperPotentialHomeOvernightStayIntervalEvent(
sessionId,
"12:123",
@ -179,6 +201,7 @@ class TachographFileSessionControllerTest {
"VIN-1",
"VIN-2"
)),
List.of(),
List.of(new TachographEsperVehicleUsageIntervalEvent(
sessionId,
"12:123",
@ -273,7 +296,10 @@ class TachographFileSessionControllerTest {
.andExpect(jsonPath("$.drivingInterruptionIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervalCount").value(1))
.andExpect(jsonPath("$.dailyWeeklyRestCandidateIntervalCount").value(1))
.andExpect(jsonPath("$.dailyWeeklyRestCandidateCoverageIntervalCount").value(1))
.andExpect(jsonPath("$.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount").value(0))
.andExpect(jsonPath("$.potentialHomeOvernightStayIntervalCount").value(1))
.andExpect(jsonPath("$.potentialInVehicleOvernightStayIntervalCount").value(0))
.andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1"))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextRegistrationKey").value("12:REG-2"))

View File

@ -7,6 +7,7 @@ import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInter
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
@ -122,8 +123,99 @@ class DriverTimelineReusableProjectionBuilderTest {
assertThat(reusableBundle.drivingInterruptionIntervals()).containsExactlyElementsOf(legacyDrivingInterruptions);
assertThat(reusableBundle.dailyWeeklyRestCandidateIntervals()).containsExactlyElementsOf(legacyDailyWeeklyRestCandidates);
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1);
TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent coverageInterval =
reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0);
assertThat(coverageInterval.unknownCoveragePercent()).isCloseTo(99.998d, org.assertj.core.data.Offset.offset(0.001d));
assertThat(coverageInterval.cardPresentCoveragePercent()).isEqualTo(0.0d);
assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).containsExactlyElementsOf(legacyDrivingInterruptionVehicleChanges);
assertThat(reusableBundle.vuCardAbsentIntervals()).containsExactlyElementsOf(legacyVuCardAbsentIntervals);
assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).containsExactlyElementsOf(legacyPotentialHomeOvernightStays);
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).isEmpty();
}
@Test
void buildsPotentialInVehicleOvernightStayIntervalsFromVehicleUnchangedCoveredRest() {
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-02T01:00:00Z"),
100L,
260L,
"12:REG-1",
"VIN-1",
"vu-1"
)
),
List.of(
new ExtractedCardActivityInterval(
"ACT-1",
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
"DRIVE",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-1",
"VIN-1",
"a"
),
new ExtractedCardActivityInterval(
"ACT-2",
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T00: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, 1, 1, 1, 0),
List.of(),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
ResolvedDriverTimeline timeline = legacyBuilder.build(session, driver);
TachographEsperDrivingDerivedProjectionBundle reusableBundle =
reusableBuilder.buildEsperDrivingDerivedProjectionBundle(
session.sessionId(),
driver.driverKey(),
timeline,
3,
720
);
assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).isEmpty();
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1);
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent())
.isEqualTo(100.0d);
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent())
.isEqualTo(0.0d);
assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).isEmpty();
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).hasSize(1);
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).startedAt())
.isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).endedAt())
.isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z"));
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).cardPresentCoveragePercent())
.isEqualTo(100.0d);
}
}

View File

@ -100,7 +100,10 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0);
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
assertThat(result.vuCardAbsentIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:01Z"));
@ -194,7 +197,10 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0);
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z"));
assertThat(result.drivingInterruptionIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
assertThat(result.drivingInterruptionIntervals().get(0).previousRegistrationKey()).isEqualTo("12:REG-1");
@ -291,6 +297,9 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.activityIntervalCount()).isEqualTo(3);
assertThat(result.drivingIntervalCount()).isEqualTo(2);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0);
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0);
assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
}
@ -373,19 +382,200 @@ class TachographFileSessionProcessingServiceTest {
);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(1);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1);
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(1);
assertThat(result.dailyWeeklyRestCandidateIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z"));
assertThat(result.dailyWeeklyRestCandidateIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T23:00:00Z"));
assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z"));
assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownDurationSeconds()).isEqualTo(43_200L);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent()).isEqualTo(100.0d);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(0L);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(0.0d);
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z"));
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).previousRegistrationKey()).isEqualTo("12:REG-1");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(1);
assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z"));
assertThat(result.potentialHomeOvernightStayIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T23:00:00Z"));
assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownDurationSeconds()).isEqualTo(43_200L);
assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownCoveragePercent()).isEqualTo(100.0d);
}
@Test
void returnsPotentialInVehicleOvernightStayIntervalsWhenVehicleUnchangedAndCardRemainsInserted() {
EventHubProperties properties = new EventHubProperties();
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder();
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository,
driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder,
new DriverKeyFactory(),
new VehicleKeyFactory(),
new EventDetailsFactory(new ObjectMapper())
)
),
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-02T01:00:00Z"),
100L,
260L,
"12:REG-1",
"VIN-1",
"vu-1"
)
),
List.of(
new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"),
new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-02T00:00:00Z"), OffsetDateTime.parse("2026-05-02T00: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, 1, 1, 1, 0),
List.of(),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
repository.save(session);
TachographEsperDriverProcessingResultDto result = service.getEsperDriverProcessingResults(
session.sessionId(),
driver.driverKey(),
new TachographEsperEventsProcessingRequest(
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
3,
720
)
);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(1);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1);
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(1);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(50_400L);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(100.0d);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownDurationSeconds()).isEqualTo(0L);
assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z"));
assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(50_400L);
assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(100.0d);
}
@Test
void returnsUnclassifiedDailyWeeklyRestCandidateCoverageIntervalsWhenRestCandidateFitsNeitherCategory() {
EventHubProperties properties = new EventHubProperties();
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder();
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository,
driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder,
new DriverKeyFactory(),
new VehicleKeyFactory(),
new EventDetailsFactory(new ObjectMapper())
)
),
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-01T14:00:00Z"),
100L,
200L,
"12:REG-1",
"VIN-1",
"vu-1"
),
new ExtractedCardVehicleUsageInterval(
"CVU-2",
OffsetDateTime.parse("2026-05-01T18:00:00Z"),
OffsetDateTime.parse("2026-05-02T02:00:00Z"),
201L,
260L,
"12:REG-1",
"VIN-1",
"vu-2"
)
),
List.of(
new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"),
new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-02T00:30:00Z"), OffsetDateTime.parse("2026-05-02T01: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", 2, "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(),
new TachographEsperEventsProcessingRequest(
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:30:00Z"),
3,
720
)
);
assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1);
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1);
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).startedAt())
.isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).endedAt())
.isEqualTo(OffsetDateTime.parse("2026-05-02T00:30:00Z"));
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent())
.isLessThan(95.0d);
assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent())
.isLessThan(95.0d);
}
@Test
void evaluatesOperatingPeriodsFromSessionTimeline() {
EventHubProperties properties = new EventHubProperties();