Add runtime tachograph parity validation

This commit is contained in:
trifonovt 2026-05-25 22:58:47 +02:00
parent e68047feab
commit 471726c4cc
10 changed files with 872 additions and 2 deletions

View File

@ -294,3 +294,60 @@ This prevents unrelated vehicle events from being copied into a driver result si
`parameters` take precedence in the tachograph profile. The compatibility endpoint maps these values to `expandVehicleEvents` and `vehicleExpansionPaddingMinutes`. `parameters` take precedence in the tachograph profile. The compatibility endpoint maps these values to `expandVehicleEvents` and `vehicleExpansionPaddingMinutes`.
## Tachograph parity validation
A validation endpoint is available to compare the legacy tachograph file-session Esper endpoint with the generic runtime event-processing profile:
```http
POST /api/eventhub/runtime-processing/event-processing/validation/tachograph-parity
```
This endpoint runs both paths for the selected session(s) and driver(s):
```text
legacy file-session path
/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/esper-events
runtime event-processing path
/api/eventhub/runtime-processing/event-processing
profileKey = tachograph-driver-esper-v1
```
Example request:
```json
{
"sessionId": "{{sessionId}}",
"driverKey": "{{driverKey}}",
"occurredFrom": "2026-05-01T00:00:00Z",
"occurredTo": "2026-05-31T23:59:59Z",
"expandVehicleEvents": true,
"vehicleExpansionPaddingMinutes": 15,
"significantDrivingMinutes": 3,
"minimumRestPeriodMinutes": 720,
"includeDebug": true
}
```
For multiple uploaded tachograph sessions, use `sessionIds` or `compositeSessionId`. The validation service resolves the selected drivers, executes the generic runtime profile, then compares count-level parity per driver.
Compared categories include:
```text
activityIntervals
drivingIntervals
drivingInterruptionIntervals
drivingInterruptionVehicleChangeIntervals
dailyWeeklyRestCandidateIntervals
dailyWeeklyRestCandidateCoverageIntervals
unclassifiedDailyWeeklyRestCandidateCoverageIntervals
potentialHomeOvernightStayIntervals
potentialInVehicleOvernightStayIntervals
potentialInVehicleTripIntervals
vehicleUsageIntervals
vuCardAbsentIntervals
supportGeoEvents
```
For a single session, this is a direct parity check against the original file-session endpoint. For multiple sessions, the reference side is the sum of the individual file-session endpoint results per driver; runtime processing may intentionally deduplicate or merge across session boundaries, so differences should be reviewed with the debug/audit output.

View File

@ -159,6 +159,36 @@
] ]
} }
} }
},
{
"name": "Validate tachograph profile parity - single session",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing/validation/tachograph-parity",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"event-processing",
"validation",
"tachograph-parity"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15,\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includeDebug\": true\n}"
}
}
} }
] ]
} }

View File

@ -7,6 +7,9 @@ import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingServi
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationApiRequest;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationResultDto;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationService;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.service.UnifiedRuntimeDerivedProjectionService; import at.procon.eventhub.processing.service.UnifiedRuntimeDerivedProjectionService;
import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService; import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService;
@ -31,13 +34,25 @@ public class UnifiedRuntimeProcessingController {
private final UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService; private final UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService;
private final UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService; private final UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService;
private final RuntimeEventProcessingService runtimeEventProcessingService; private final RuntimeEventProcessingService runtimeEventProcessingService;
private final RuntimeTachographParityValidationService tachographParityValidationService;
public UnifiedRuntimeProcessingController( public UnifiedRuntimeProcessingController(
UnifiedRuntimeEventAssemblyService eventAssemblyService, UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService, UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService
) { ) {
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null); this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null, null);
}
public UnifiedRuntimeProcessingController(
UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService,
RuntimeEventProcessingService runtimeEventProcessingService
) {
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService,
tachographEsperScopeProcessingService, runtimeEventProcessingService, null);
} }
@Autowired @Autowired
@ -46,13 +61,15 @@ public class UnifiedRuntimeProcessingController {
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService, UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService, UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService, UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService,
RuntimeEventProcessingService runtimeEventProcessingService RuntimeEventProcessingService runtimeEventProcessingService,
RuntimeTachographParityValidationService tachographParityValidationService
) { ) {
this.eventAssemblyService = eventAssemblyService; this.eventAssemblyService = eventAssemblyService;
this.runtimeDriverTimelineService = runtimeDriverTimelineService; this.runtimeDriverTimelineService = runtimeDriverTimelineService;
this.runtimeDerivedProjectionService = runtimeDerivedProjectionService; this.runtimeDerivedProjectionService = runtimeDerivedProjectionService;
this.tachographEsperScopeProcessingService = tachographEsperScopeProcessingService; this.tachographEsperScopeProcessingService = tachographEsperScopeProcessingService;
this.runtimeEventProcessingService = runtimeEventProcessingService; this.runtimeEventProcessingService = runtimeEventProcessingService;
this.tachographParityValidationService = tachographParityValidationService;
} }
@PostMapping("/driver-events") @PostMapping("/driver-events")
@ -94,6 +111,17 @@ public class UnifiedRuntimeProcessingController {
return ResponseEntity.ok(runtimeEventProcessingService.process(request)); return ResponseEntity.ok(runtimeEventProcessingService.process(request));
} }
@PostMapping("/event-processing/validation/tachograph-parity")
public ResponseEntity<RuntimeTachographParityValidationResultDto> validateTachographParity(
@RequestBody RuntimeTachographParityValidationApiRequest request
) {
if (tachographParityValidationService == null) {
throw new IllegalStateException("Runtime tachograph parity validation service is not configured.");
}
return ResponseEntity.ok(tachographParityValidationService.validate(request));
}
@PostMapping("/tachograph/esper-processing") @PostMapping("/tachograph/esper-processing")
public ResponseEntity<UnifiedRuntimeTachographEsperScopeResultDto> runTachographEsperProcessing( public ResponseEntity<UnifiedRuntimeTachographEsperScopeResultDto> runTachographEsperProcessing(
@RequestBody UnifiedRuntimeProcessingApiRequest request @RequestBody UnifiedRuntimeProcessingApiRequest request

View File

@ -0,0 +1,20 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import java.util.List;
public record RuntimeTachographDriverParityResultDto(
String driverKey,
String status,
String referenceMode,
int referenceSessionCount,
boolean runtimePartitionPresent,
List<RuntimeTachographParityCategoryComparisonDto> comparisons,
List<String> notes,
List<String> warnings
) {
public RuntimeTachographDriverParityResultDto {
comparisons = comparisons == null ? List.of() : List.copyOf(comparisons);
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
}

View File

@ -0,0 +1,9 @@
package at.procon.eventhub.processing.eventprocessing.validation;
public record RuntimeTachographParityCategoryComparisonDto(
String category,
int fileSessionCount,
int runtimeCount,
boolean equal
) {
}

View File

@ -0,0 +1,61 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import java.time.OffsetDateTime;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public record RuntimeTachographParityValidationApiRequest(
UUID sessionId,
List<UUID> sessionIds,
UUID compositeSessionId,
String driverKey,
Set<String> driverKeys,
Boolean includeAllDrivers,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
Boolean expandVehicleEvents,
Integer vehicleExpansionPaddingMinutes,
Integer significantDrivingMinutes,
Integer minimumRestPeriodMinutes,
Boolean includeDebug
) {
public RuntimeTachographParityValidationApiRequest {
sessionIds = sessionIds == null ? List.of() : List.copyOf(sessionIds);
driverKeys = driverKeys == null ? Set.of() : Set.copyOf(driverKeys);
vehicleExpansionPaddingMinutes = vehicleExpansionPaddingMinutes == null
? null
: Math.max(0, vehicleExpansionPaddingMinutes);
significantDrivingMinutes = significantDrivingMinutes == null ? null : Math.max(1, significantDrivingMinutes);
minimumRestPeriodMinutes = minimumRestPeriodMinutes == null ? null : Math.max(1, minimumRestPeriodMinutes);
}
public Set<String> requestedDriverKeys() {
LinkedHashSet<String> result = new LinkedHashSet<>();
if (driverKey != null && !driverKey.isBlank()) {
result.add(driverKey.trim());
}
driverKeys.stream()
.filter(value -> value != null && !value.isBlank())
.map(String::trim)
.forEach(result::add);
return Set.copyOf(result);
}
public boolean includeAllDriversOrDefault() {
return includeAllDrivers == null ? requestedDriverKeys().isEmpty() : includeAllDrivers;
}
public boolean expandVehicleEventsOrDefault() {
return expandVehicleEvents == null || expandVehicleEvents;
}
public boolean includeDebugOrDefault() {
return includeDebug != null && includeDebug;
}
public int vehicleExpansionPaddingMinutesOrDefault() {
return vehicleExpansionPaddingMinutes == null ? 0 : Math.max(0, vehicleExpansionPaddingMinutes);
}
}

View File

@ -0,0 +1,23 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public record RuntimeTachographParityValidationResultDto(
String profileKey,
String status,
List<UUID> sessionIds,
int selectedDriverCount,
Map<String, RuntimeTachographDriverParityResultDto> driverResults,
List<String> notes,
List<String> warnings
) {
public RuntimeTachographParityValidationResultDto {
sessionIds = sessionIds == null ? List.of() : List.copyOf(sessionIds);
driverResults = driverResults == null ? Map.of() : Map.copyOf(new LinkedHashMap<>(driverResults));
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
}

View File

@ -0,0 +1,388 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import at.procon.eventhub.processing.eventprocessing.profile.TachographDriverEsperRuntimeEventProcessingProfile;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.model.TachographCompositeSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionRepository;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.springframework.stereotype.Service;
@Service
public class RuntimeTachographParityValidationService {
private final TachographFileSessionRepository fileSessionRepository;
private final TachographCompositeSessionRepository compositeSessionRepository;
private final TachographFileSessionProcessingService fileSessionProcessingService;
private final RuntimeEventProcessingService runtimeEventProcessingService;
public RuntimeTachographParityValidationService(
TachographFileSessionRepository fileSessionRepository,
TachographCompositeSessionRepository compositeSessionRepository,
TachographFileSessionProcessingService fileSessionProcessingService,
RuntimeEventProcessingService runtimeEventProcessingService
) {
this.fileSessionRepository = fileSessionRepository;
this.compositeSessionRepository = compositeSessionRepository;
this.fileSessionProcessingService = fileSessionProcessingService;
this.runtimeEventProcessingService = runtimeEventProcessingService;
}
public RuntimeTachographParityValidationResultDto validate(RuntimeTachographParityValidationApiRequest request) {
RuntimeTachographParityValidationApiRequest effectiveRequest = request == null
? new RuntimeTachographParityValidationApiRequest(null, List.of(), null, null, Set.of(), true,
null, null, true, 0, null, null, false)
: request;
List<UUID> sessionIds = resolveSessionIds(effectiveRequest);
Set<String> driverKeys = resolveDriverKeys(effectiveRequest, sessionIds);
if (driverKeys.isEmpty()) {
throw new IllegalArgumentException("No driver keys could be resolved for tachograph parity validation.");
}
RuntimeEventProcessingResultDto runtimeResult = runtimeEventProcessingService.process(genericRuntimeRequest(
effectiveRequest,
sessionIds,
driverKeys
));
LinkedHashMap<String, RuntimeTachographDriverParityResultDto> driverResults = new LinkedHashMap<>();
List<String> warnings = new ArrayList<>(runtimeResult.warnings());
for (String driverKey : driverKeys) {
RuntimeTachographDriverParityResultDto driverResult = compareDriver(
effectiveRequest,
sessionIds,
driverKey,
runtimeResult
);
driverResults.put(driverKey, driverResult);
warnings.addAll(driverResult.warnings());
}
String status = driverResults.values().stream().allMatch(result -> "EQUAL".equals(result.status()))
? "EQUAL"
: "DIFFERENT";
List<String> notes = new ArrayList<>(runtimeResult.notes());
notes.add("Validation compares the legacy tachograph file-session esper-events path with the generic runtime event-processing profile '"
+ TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY + "'.");
if (sessionIds.size() > 1) {
notes.add("For multiple sessions, file-session reference counts are summed per driver. Runtime processing may intentionally merge or deduplicate intervals across session boundaries, so category differences need domain review.");
}
return new RuntimeTachographParityValidationResultDto(
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
status,
sessionIds,
driverResults.size(),
driverResults,
notes,
warnings
);
}
private RuntimeTachographDriverParityResultDto compareDriver(
RuntimeTachographParityValidationApiRequest request,
List<UUID> sessionIds,
String driverKey,
RuntimeEventProcessingResultDto runtimeResult
) {
RuntimeProjectionCounts runtimeCounts = runtimeCounts(runtimeResult, driverKey);
ReferenceProjectionCounts referenceCounts = referenceCounts(request, sessionIds, driverKey);
List<RuntimeTachographParityCategoryComparisonDto> comparisons = comparisons(referenceCounts.counts(), runtimeCounts.counts());
boolean runtimePresent = runtimeCounts.present();
boolean referencePresent = referenceCounts.referenceSessionCount() > 0;
boolean allEqual = comparisons.stream().allMatch(RuntimeTachographParityCategoryComparisonDto::equal);
String status;
if (!referencePresent) {
status = "REFERENCE_MISSING";
} else if (!runtimePresent) {
status = "RUNTIME_MISSING";
} else if (allEqual) {
status = "EQUAL";
} else {
status = "DIFFERENT";
}
List<String> notes = new ArrayList<>();
notes.add("Reference mode: " + referenceCounts.referenceMode() + ".");
notes.add("Reference session count: " + referenceCounts.referenceSessionCount() + ".");
if (sessionIds.size() > 1) {
notes.add("Reference counts are summed from individual file-session endpoint results for this driver.");
}
List<String> warnings = new ArrayList<>();
if (!referencePresent) {
warnings.add("Driver " + driverKey + " was not found in any selected tachograph file session.");
}
if (!runtimePresent) {
warnings.add("Generic runtime event-processing profile did not return a partition for driver " + driverKey + ".");
}
if (runtimePresent && referencePresent && !allEqual) {
warnings.add("Runtime result differs from file-session reference result for driver " + driverKey + ".");
}
return new RuntimeTachographDriverParityResultDto(
driverKey,
status,
referenceCounts.referenceMode(),
referenceCounts.referenceSessionCount(),
runtimePresent,
comparisons,
notes,
warnings
);
}
private RuntimeEventProcessingApiRequest genericRuntimeRequest(
RuntimeTachographParityValidationApiRequest request,
List<UUID> sessionIds,
Set<String> driverKeys
) {
UnifiedRuntimeProcessingApiRequest scope = new UnifiedRuntimeProcessingApiRequest(
null,
sessionIds,
null,
null,
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
UnifiedRuntimeEventBackend.SOURCE_DB,
null,
driverKeys,
request.includeAllDriversOrDefault(),
null,
false,
null,
null,
null,
request.occurredFrom(),
request.occurredTo(),
request.expandVehicleEventsOrDefault(),
request.vehicleExpansionPaddingMinutesOrDefault(),
request.significantDrivingMinutes(),
request.minimumRestPeriodMinutes()
);
RuntimeEventPartitioningApiRequest partitioning = new RuntimeEventPartitioningApiRequest(
RuntimeEventPartitioningStrategy.DRIVER,
null,
request.includeAllDriversOrDefault(),
driverKeys,
request.includeAllDriversOrDefault(),
null,
false,
request.expandVehicleEventsOrDefault(),
request.vehicleExpansionPaddingMinutesOrDefault(),
request.includeDebugOrDefault()
);
Map<String, Object> parameters = new LinkedHashMap<>();
if (request.significantDrivingMinutes() != null) {
parameters.put("significantDrivingMinutes", request.significantDrivingMinutes());
}
if (request.minimumRestPeriodMinutes() != null) {
parameters.put("minimumRestPeriodMinutes", request.minimumRestPeriodMinutes());
}
parameters.put("attachVehicleOnlyEvents", request.expandVehicleEventsOrDefault());
parameters.put("vehicleEvidencePaddingMinutes", request.vehicleExpansionPaddingMinutesOrDefault());
parameters.put("includePartitionDebug", request.includeDebugOrDefault());
return new RuntimeEventProcessingApiRequest(
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
scope,
partitioning,
parameters
);
}
private List<UUID> resolveSessionIds(RuntimeTachographParityValidationApiRequest request) {
LinkedHashSet<UUID> result = new LinkedHashSet<>();
if (request.sessionId() != null) {
result.add(request.sessionId());
}
result.addAll(request.sessionIds());
if (request.compositeSessionId() != null) {
TachographCompositeSession compositeSession = compositeSessionRepository.find(request.compositeSessionId())
.orElseThrow(() -> new TachographCompositeSessionNotFoundException(request.compositeSessionId()));
result.addAll(compositeSession.memberSessionIds());
}
if (result.isEmpty()) {
throw new IllegalArgumentException("sessionId, sessionIds, or compositeSessionId is required for tachograph parity validation.");
}
result.forEach(this::requireFileSession);
return List.copyOf(result);
}
private Set<String> resolveDriverKeys(RuntimeTachographParityValidationApiRequest request, List<UUID> sessionIds) {
LinkedHashSet<String> result = new LinkedHashSet<>(request.requestedDriverKeys());
if (request.includeAllDriversOrDefault() || result.isEmpty()) {
for (UUID sessionId : sessionIds) {
result.addAll(requireFileSession(sessionId).driversByKey().keySet());
}
}
return Set.copyOf(result);
}
private TachographFileSession requireFileSession(UUID sessionId) {
return fileSessionRepository.find(sessionId)
.orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
}
private ReferenceProjectionCounts referenceCounts(
RuntimeTachographParityValidationApiRequest request,
List<UUID> sessionIds,
String driverKey
) {
ProjectionCounts aggregate = ProjectionCounts.empty();
int referenceSessionCount = 0;
TachographEsperEventsProcessingRequest fileSessionRequest = new TachographEsperEventsProcessingRequest(
request.occurredFrom(),
request.occurredTo(),
request.significantDrivingMinutes(),
request.minimumRestPeriodMinutes()
);
for (UUID sessionId : sessionIds) {
TachographFileSession session = requireFileSession(sessionId);
if (!session.driversByKey().containsKey(driverKey)) {
continue;
}
TachographEsperDriverProcessingResultDto projection = fileSessionProcessingService.getEsperDriverProcessingResults(
sessionId,
driverKey,
fileSessionRequest
);
aggregate = aggregate.plus(ProjectionCounts.from(projection));
referenceSessionCount++;
}
String referenceMode = sessionIds.size() == 1 ? "SINGLE_FILE_SESSION" : "SUM_OF_FILE_SESSION_RESULTS";
return new ReferenceProjectionCounts(aggregate, referenceMode, referenceSessionCount);
}
private RuntimeProjectionCounts runtimeCounts(RuntimeEventProcessingResultDto runtimeResult, String driverKey) {
RuntimeEventProcessingPartitionResultDto partitionResult = runtimeResult.partitionResults().get(driverKey);
if (partitionResult == null) {
return new RuntimeProjectionCounts(ProjectionCounts.empty(), false);
}
Object result = partitionResult.result();
if (result instanceof UnifiedRuntimeDerivedProjectionResultDto projectionResult) {
return new RuntimeProjectionCounts(ProjectionCounts.from(projectionResult.projection()), projectionResult.projection() != null);
}
return new RuntimeProjectionCounts(ProjectionCounts.empty(), false);
}
private List<RuntimeTachographParityCategoryComparisonDto> comparisons(
ProjectionCounts reference,
ProjectionCounts runtime
) {
return List.of(
comparison("activityIntervals", reference.activityIntervalCount(), runtime.activityIntervalCount()),
comparison("drivingIntervals", reference.drivingIntervalCount(), runtime.drivingIntervalCount()),
comparison("drivingInterruptionIntervals", reference.drivingInterruptionIntervalCount(), runtime.drivingInterruptionIntervalCount()),
comparison("drivingInterruptionVehicleChangeIntervals", reference.drivingInterruptionVehicleChangeIntervalCount(), runtime.drivingInterruptionVehicleChangeIntervalCount()),
comparison("dailyWeeklyRestCandidateIntervals", reference.dailyWeeklyRestCandidateIntervalCount(), runtime.dailyWeeklyRestCandidateIntervalCount()),
comparison("dailyWeeklyRestCandidateCoverageIntervals", reference.dailyWeeklyRestCandidateCoverageIntervalCount(), runtime.dailyWeeklyRestCandidateCoverageIntervalCount()),
comparison("unclassifiedDailyWeeklyRestCandidateCoverageIntervals", reference.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(), runtime.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()),
comparison("potentialHomeOvernightStayIntervals", reference.potentialHomeOvernightStayIntervalCount(), runtime.potentialHomeOvernightStayIntervalCount()),
comparison("potentialInVehicleOvernightStayIntervals", reference.potentialInVehicleOvernightStayIntervalCount(), runtime.potentialInVehicleOvernightStayIntervalCount()),
comparison("potentialInVehicleTripIntervals", reference.potentialInVehicleTripIntervalCount(), runtime.potentialInVehicleTripIntervalCount()),
comparison("vehicleUsageIntervals", reference.vehicleUsageIntervalCount(), runtime.vehicleUsageIntervalCount()),
comparison("vuCardAbsentIntervals", reference.vuCardAbsentIntervalCount(), runtime.vuCardAbsentIntervalCount()),
comparison("supportGeoEvents", reference.supportGeoEventCount(), runtime.supportGeoEventCount())
);
}
private RuntimeTachographParityCategoryComparisonDto comparison(String category, int fileSessionCount, int runtimeCount) {
return new RuntimeTachographParityCategoryComparisonDto(
category,
fileSessionCount,
runtimeCount,
fileSessionCount == runtimeCount
);
}
private record ReferenceProjectionCounts(
ProjectionCounts counts,
String referenceMode,
int referenceSessionCount
) {
}
private record RuntimeProjectionCounts(
ProjectionCounts counts,
boolean present
) {
}
private record ProjectionCounts(
int activityIntervalCount,
int drivingIntervalCount,
int drivingInterruptionIntervalCount,
int drivingInterruptionVehicleChangeIntervalCount,
int dailyWeeklyRestCandidateIntervalCount,
int dailyWeeklyRestCandidateCoverageIntervalCount,
int unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
int potentialHomeOvernightStayIntervalCount,
int potentialInVehicleOvernightStayIntervalCount,
int potentialInVehicleTripIntervalCount,
int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount,
int supportGeoEventCount
) {
static ProjectionCounts empty() {
return new ProjectionCounts(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
static ProjectionCounts from(TachographEsperDriverProcessingResultDto projection) {
if (projection == null) {
return empty();
}
return new ProjectionCounts(
projection.activityIntervalCount(),
projection.drivingIntervalCount(),
projection.drivingInterruptionIntervalCount(),
projection.drivingInterruptionVehicleChangeIntervalCount(),
projection.dailyWeeklyRestCandidateIntervalCount(),
projection.dailyWeeklyRestCandidateCoverageIntervalCount(),
projection.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(),
projection.potentialHomeOvernightStayIntervalCount(),
projection.potentialInVehicleOvernightStayIntervalCount(),
projection.potentialInVehicleTripIntervalCount(),
projection.vehicleUsageIntervalCount(),
projection.vuCardAbsentIntervalCount(),
projection.supportGeoEventCount()
);
}
ProjectionCounts plus(ProjectionCounts other) {
return new ProjectionCounts(
activityIntervalCount + other.activityIntervalCount,
drivingIntervalCount + other.drivingIntervalCount,
drivingInterruptionIntervalCount + other.drivingInterruptionIntervalCount,
drivingInterruptionVehicleChangeIntervalCount + other.drivingInterruptionVehicleChangeIntervalCount,
dailyWeeklyRestCandidateIntervalCount + other.dailyWeeklyRestCandidateIntervalCount,
dailyWeeklyRestCandidateCoverageIntervalCount + other.dailyWeeklyRestCandidateCoverageIntervalCount,
unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount + other.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
potentialHomeOvernightStayIntervalCount + other.potentialHomeOvernightStayIntervalCount,
potentialInVehicleOvernightStayIntervalCount + other.potentialInVehicleOvernightStayIntervalCount,
potentialInVehicleTripIntervalCount + other.potentialInVehicleTripIntervalCount,
vehicleUsageIntervalCount + other.vehicleUsageIntervalCount,
vuCardAbsentIntervalCount + other.vuCardAbsentIntervalCount,
supportGeoEventCount + other.supportGeoEventCount
);
}
}
}

View File

@ -21,6 +21,10 @@ import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingR
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographDriverParityResultDto;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityCategoryComparisonDto;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationResultDto;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationService;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend; import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
@ -447,6 +451,71 @@ class UnifiedRuntimeProcessingControllerTest {
.andExpect(jsonPath("$.notes[0]").value("generic adapter")); .andExpect(jsonPath("$.notes[0]").value("generic adapter"));
} }
@Test
void validatesTachographParityViaRuntimeApi() throws Exception {
UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);
UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class);
UnifiedRuntimeDerivedProjectionService derivedProjectionService = org.mockito.Mockito.mock(UnifiedRuntimeDerivedProjectionService.class);
RuntimeEventProcessingService runtimeEventProcessingService = org.mockito.Mockito.mock(RuntimeEventProcessingService.class);
RuntimeTachographParityValidationService parityValidationService = org.mockito.Mockito.mock(RuntimeTachographParityValidationService.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(
eventAssemblyService,
timelineService,
derivedProjectionService,
null,
runtimeEventProcessingService,
parityValidationService
))
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
.build();
UUID sessionId = UUID.randomUUID();
when(parityValidationService.validate(any()))
.thenReturn(new RuntimeTachographParityValidationResultDto(
"tachograph-driver-esper-v1",
"EQUAL",
List.of(sessionId),
1,
Map.of("12:123", new RuntimeTachographDriverParityResultDto(
"12:123",
"EQUAL",
"SINGLE_FILE_SESSION",
1,
true,
List.of(new RuntimeTachographParityCategoryComparisonDto(
"activityIntervals",
2,
2,
true
)),
List.of("validated"),
List.of()
)),
List.of("validation complete"),
List.of()
));
mockMvc.perform(post("/api/eventhub/runtime-processing/event-processing/validation/tachograph-parity")
.contentType("application/json")
.content("""
{
"sessionId": "%s",
"driverKey": "12:123",
"occurredFrom": "2026-05-01T08:00:00Z",
"occurredTo": "2026-05-01T10:00:00Z",
"includeDebug": true
}
""".formatted(sessionId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.profileKey").value("tachograph-driver-esper-v1"))
.andExpect(jsonPath("$.status").value("EQUAL"))
.andExpect(jsonPath("$.driverResults['12:123'].status").value("EQUAL"))
.andExpect(jsonPath("$.driverResults['12:123'].comparisons[0].category").value("activityIntervals"))
.andExpect(jsonPath("$.driverResults['12:123'].comparisons[0].equal").value(true));
}
@Test @Test
void returnsBadRequestForInvalidRuntimeRequest() throws Exception { void returnsBadRequestForInvalidRuntimeRequest() throws Exception {
UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class); UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);

View File

@ -0,0 +1,185 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import at.procon.eventhub.processing.eventprocessing.profile.TachographDriverEsperRuntimeEventProcessingProfile;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionRepository;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
class RuntimeTachographParityValidationServiceTest {
@Test
void reportsEqualWhenRuntimeProfileMatchesFileSessionReferenceCounts() {
TachographFileSessionRepository fileSessionRepository = org.mockito.Mockito.mock(TachographFileSessionRepository.class);
TachographCompositeSessionRepository compositeSessionRepository = org.mockito.Mockito.mock(TachographCompositeSessionRepository.class);
TachographFileSessionProcessingService fileSessionProcessingService = org.mockito.Mockito.mock(TachographFileSessionProcessingService.class);
RuntimeEventProcessingService runtimeEventProcessingService = org.mockito.Mockito.mock(RuntimeEventProcessingService.class);
RuntimeTachographParityValidationService service = new RuntimeTachographParityValidationService(
fileSessionRepository,
compositeSessionRepository,
fileSessionProcessingService,
runtimeEventProcessingService
);
UUID sessionId = UUID.randomUUID();
String driverKey = "12:123";
TachographFileSession session = session(sessionId, driverKey);
when(fileSessionRepository.find(sessionId)).thenReturn(Optional.of(session));
when(fileSessionProcessingService.getEsperDriverProcessingResults(eq(sessionId), eq(driverKey), any()))
.thenReturn(projection(sessionId, driverKey, 2, 1, 1));
UnifiedRuntimeProcessingRequest runtimeRequest = UnifiedRuntimeProcessingRequest.forTachographFileSession(
sessionId,
driverKey,
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
true,
0
);
UnifiedRuntimeDerivedProjectionResultDto runtimeDriverResult = new UnifiedRuntimeDerivedProjectionResultDto(
runtimeRequest,
4,
1,
0,
4,
List.of(),
projection(sessionId, driverKey, 2, 1, 1),
List.of()
);
when(runtimeEventProcessingService.process(any()))
.thenReturn(new RuntimeEventProcessingResultDto(
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
RuntimeEventPartitioningStrategy.DRIVER,
runtimeRequest,
4,
1,
1,
List.of(),
Map.of(driverKey, new RuntimeEventProcessingPartitionResultDto(
"DRIVER",
driverKey,
"UnifiedRuntimeDerivedProjectionResultDto",
runtimeDriverResult,
Map.of()
)),
List.of(),
List.of()
));
RuntimeTachographParityValidationResultDto result = service.validate(new RuntimeTachographParityValidationApiRequest(
sessionId,
List.of(),
null,
driverKey,
Set.of(),
false,
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
true,
0,
3,
720,
true
));
assertThat(result.status()).isEqualTo("EQUAL");
assertThat(result.driverResults()).containsKey(driverKey);
assertThat(result.driverResults().get(driverKey).comparisons())
.anySatisfy(comparison -> {
assertThat(comparison.category()).isEqualTo("activityIntervals");
assertThat(comparison.fileSessionCount()).isEqualTo(2);
assertThat(comparison.runtimeCount()).isEqualTo(2);
assertThat(comparison.equal()).isTrue();
});
}
private TachographFileSession session(UUID sessionId, String driverKey) {
DriverExtractionSession driver = new DriverExtractionSession(
driverKey,
null,
null,
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of()
);
return new TachographFileSession(
sessionId,
null,
Map.of(driverKey, driver),
new ExtractionStats(1, 0, 0, 0, 0, 0),
List.of(),
Instant.now(),
Instant.now().plusSeconds(3600)
);
}
private TachographEsperDriverProcessingResultDto projection(
UUID sessionId,
String driverKey,
int activityCount,
int drivingCount,
int interruptionCount
) {
return new TachographEsperDriverProcessingResultDto(
sessionId,
driverKey,
"DRIVER_CARD",
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
activityCount,
drivingCount,
interruptionCount,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of()
);
}
}