Add runtime vehicle evidence debug output

This commit is contained in:
trifonovt 2026-05-25 22:44:42 +02:00
parent b04b333db7
commit e68047feab
18 changed files with 456 additions and 42 deletions

17
README_PATCH.md Normal file
View File

@ -0,0 +1,17 @@
# EventHub fix-list patch
This patch implements the requested remaining items from the fix list, excluding build verification and SQL Server 2008 SQL rewriting.
## Apply notes
1. Copy the files from this archive into the project root.
2. Delete the old migration files listed in `DELETE_FILES.txt`.
3. Run the test suite locally with Maven/Java 21.
## Main changes
- Renumbered Flyway migrations to remove duplicate `V9` and `V10` versions.
- Removed the duplicated Timescale/Event source record migration.
- Switched local Docker Compose DB from plain PostgreSQL to a TimescaleDB/PostGIS-capable image.
- Added normalized raw tachograph payload metadata for DB-extracted EventHub events.
- Added tests for Flyway version uniqueness and tachograph DB mapper → timeline reconstruction metadata.

View File

@ -29,7 +29,8 @@ Example response:
"significantDrivingMinutes", "significantDrivingMinutes",
"minimumRestPeriodMinutes", "minimumRestPeriodMinutes",
"attachVehicleOnlyEvents", "attachVehicleOnlyEvents",
"vehicleEvidencePaddingMinutes" "vehicleEvidencePaddingMinutes",
"includePartitionDebug"
] ]
} }
] ]
@ -63,13 +64,15 @@ POST /api/eventhub/runtime-processing/event-processing
"strategy": "DRIVER", "strategy": "DRIVER",
"includeAllPartitions": true, "includeAllPartitions": true,
"attachVehicleEvidence": true, "attachVehicleEvidence": true,
"vehicleEvidencePaddingMinutes": 15 "vehicleEvidencePaddingMinutes": 15,
"includeDebug": true
}, },
"parameters": { "parameters": {
"significantDrivingMinutes": 3, "significantDrivingMinutes": 3,
"minimumRestPeriodMinutes": 720, "minimumRestPeriodMinutes": 720,
"attachVehicleOnlyEvents": true, "attachVehicleOnlyEvents": true,
"vehicleEvidencePaddingMinutes": 15 "vehicleEvidencePaddingMinutes": 15,
"includePartitionDebug": true
} }
} }
``` ```
@ -182,6 +185,51 @@ CUSTOM_PROFILE
The first tachograph profile currently supports `DRIVER` partitioning. The service partitions mixed event scopes in Java before invoking Esper so that existing single-driver EPL windows cannot mix driver states. The first tachograph profile currently supports `DRIVER` partitioning. The service partitions mixed event scopes in Java before invoking Esper so that existing single-driver EPL windows cannot mix driver states.
## Partition debug / audit output
For mixed-source scopes, request debug output when validating why events were or were not attached to a driver partition:
```json
{
"partitioning": {
"strategy": "DRIVER",
"includeAllPartitions": true,
"attachVehicleEvidence": true,
"vehicleEvidencePaddingMinutes": 15,
"includeDebug": true
},
"parameters": {
"includePartitionDebug": true
}
}
```
When enabled, each generic `partitionResults[*].metadata.partitionDebug` contains:
```text
directDriverEventCount
vehicleUsageIntervalCount
candidateVehicleEvidenceEventCount
attachedVehicleEvidenceEventCount
ignoredVehicleEvidenceEventCount
mergedEventCount
vehicleUsageIntervals
vehicleEvidenceDecisions
notes
warnings
```
`vehicleEvidenceDecisions` explains every relevant decision, for example:
```text
DIRECT_DRIVER_EVENT
ATTACHED_VEHICLE_EVIDENCE
IGNORED_NO_OVERLAPPING_VEHICLE_USAGE
IGNORED_ATTACHMENT_DISABLED
```
The compatibility response type also has `partitionDebugByDriver`; use the generic endpoint when you need to enable debug output explicitly. Keep debug disabled for high-volume production requests unless you need attribution diagnostics, because the decision list can be large.
## Compatibility endpoint ## Compatibility endpoint
The old tachograph endpoint remains available: The old tachograph endpoint remains available:
@ -233,11 +281,13 @@ This prevents unrelated vehicle events from being copied into a driver result si
{ {
"partitioning": { "partitioning": {
"attachVehicleEvidence": true, "attachVehicleEvidence": true,
"vehicleEvidencePaddingMinutes": 15 "vehicleEvidencePaddingMinutes": 15,
"includeDebug": true
}, },
"parameters": { "parameters": {
"attachVehicleOnlyEvents": true, "attachVehicleOnlyEvents": true,
"vehicleEvidencePaddingMinutes": 15 "vehicleEvidencePaddingMinutes": 15,
"includePartitionDebug": true
} }
} }
``` ```

View File

@ -47,3 +47,8 @@ to the generic profile behavior:
``` ```
Attachment is temporal: a vehicle-only event must match a reconstructed driver vehicle-usage interval and occur inside the interval plus configured padding. Attachment is temporal: a vehicle-only event must match a reconstructed driver vehicle-usage interval and occur inside the interval plus configured padding.
## Debugging vehicle evidence attachment
Prefer the generic `/api/eventhub/runtime-processing/event-processing` endpoint with `partitioning.includeDebug=true` or `parameters.includePartitionDebug=true`. The compatibility response type has `partitionDebugByDriver`, but the generic endpoint is the preferred way to enable debug output explicitly. The generic response exposes debug data under `partitionResults[*].metadata.partitionDebug`.

View File

@ -59,7 +59,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"partitionKeys\": [\n \"{{driverKey}}\"\n ],\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n }\n}" "raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"partitionKeys\": [\n \"{{driverKey}}\"\n ],\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includePartitionDebug\": true\n }\n}"
}, },
"url": { "url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing", "raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing",
@ -87,7 +87,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n }\n}" "raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includePartitionDebug\": true\n }\n}"
}, },
"url": { "url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing", "raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing",
@ -115,7 +115,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n }\n}" "raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includePartitionDebug\": true\n }\n}"
}, },
"url": { "url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing", "raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing",

View File

@ -0,0 +1,24 @@
package at.procon.eventhub.processing.dto;
import java.util.List;
public record RuntimeDriverPartitionDebugDto(
String driverKey,
int directDriverEventCount,
int vehicleUsageIntervalCount,
int candidateVehicleEvidenceEventCount,
int attachedVehicleEvidenceEventCount,
int ignoredVehicleEvidenceEventCount,
int mergedEventCount,
List<RuntimeVehicleUsageIntervalDebugDto> vehicleUsageIntervals,
List<RuntimeVehicleEvidenceAttachmentDecisionDto> vehicleEvidenceDecisions,
List<String> notes,
List<String> warnings
) {
public RuntimeDriverPartitionDebugDto {
vehicleUsageIntervals = vehicleUsageIntervals == null ? List.of() : List.copyOf(vehicleUsageIntervals);
vehicleEvidenceDecisions = vehicleEvidenceDecisions == null ? List.of() : List.copyOf(vehicleEvidenceDecisions);
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
}

View File

@ -0,0 +1,25 @@
package at.procon.eventhub.processing.dto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeType;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Set;
public record RuntimeVehicleEvidenceAttachmentDecisionDto(
String decision,
String reason,
String eventKey,
String externalSourceEventId,
OffsetDateTime occurredAt,
String eventDomain,
String eventType,
String lifecycle,
RuntimeEventScopeType scopeType,
Set<String> vehicleKeys,
List<String> matchingVehicleUsageIntervalIds
) {
public RuntimeVehicleEvidenceAttachmentDecisionDto {
vehicleKeys = vehicleKeys == null ? Set.of() : Set.copyOf(vehicleKeys);
matchingVehicleUsageIntervalIds = matchingVehicleUsageIntervalIds == null ? List.of() : List.copyOf(matchingVehicleUsageIntervalIds);
}
}

View File

@ -0,0 +1,27 @@
package at.procon.eventhub.processing.dto;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import java.time.OffsetDateTime;
public record RuntimeVehicleUsageIntervalDebugDto(
String intervalId,
OffsetDateTime from,
OffsetDateTime to,
String registrationKey,
String vehicleKey,
String sourceKind
) {
public static RuntimeVehicleUsageIntervalDebugDto from(ResolvedVehicleUsageInterval interval) {
if (interval == null) {
return null;
}
return new RuntimeVehicleUsageIntervalDebugDto(
interval.intervalId(),
interval.from(),
interval.to(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind()
);
}
}

View File

@ -13,10 +13,48 @@ public record UnifiedRuntimeDerivedProjectionResultDto(
int mergedEventCount, int mergedEventCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles, List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
TachographEsperDriverProcessingResultDto projection, TachographEsperDriverProcessingResultDto projection,
List<String> notes List<String> notes,
RuntimeDriverPartitionDebugDto partitionDebug
) { ) {
public UnifiedRuntimeDerivedProjectionResultDto { public UnifiedRuntimeDerivedProjectionResultDto {
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles); discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
notes = notes == null ? List.of() : List.copyOf(notes); notes = notes == null ? List.of() : List.copyOf(notes);
} }
public UnifiedRuntimeDerivedProjectionResultDto(
UnifiedRuntimeProcessingRequest request,
int driverSeedEventCount,
int discoveredVehicleCount,
int expandedVehicleEventCount,
int mergedEventCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
TachographEsperDriverProcessingResultDto projection,
List<String> notes
) {
this(
request,
driverSeedEventCount,
discoveredVehicleCount,
expandedVehicleEventCount,
mergedEventCount,
discoveredVehicles,
projection,
notes,
null
);
}
public UnifiedRuntimeDerivedProjectionResultDto withPartitionDebug(RuntimeDriverPartitionDebugDto debug) {
return new UnifiedRuntimeDerivedProjectionResultDto(
request,
driverSeedEventCount,
discoveredVehicleCount,
expandedVehicleEventCount,
mergedEventCount,
discoveredVehicles,
projection,
notes,
debug
);
}
} }

View File

@ -16,12 +16,14 @@ public record UnifiedRuntimeTachographEsperScopeResultDto(
int discoveredVehicleCount, int discoveredVehicleCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles, List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults, Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults,
Map<String, RuntimeDriverPartitionDebugDto> partitionDebugByDriver,
List<String> notes, List<String> notes,
List<String> warnings List<String> warnings
) { ) {
public UnifiedRuntimeTachographEsperScopeResultDto { public UnifiedRuntimeTachographEsperScopeResultDto {
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles); discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
driverResults = driverResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(driverResults)); driverResults = driverResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(driverResults));
partitionDebugByDriver = partitionDebugByDriver == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitionDebugByDriver));
notes = notes == null ? List.of() : List.copyOf(notes); notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings); warnings = warnings == null ? List.of() : List.copyOf(warnings);
} }
@ -50,9 +52,23 @@ public record UnifiedRuntimeTachographEsperScopeResultDto(
genericResult.discoveredVehicleCount(), genericResult.discoveredVehicleCount(),
genericResult.discoveredVehicles(), genericResult.discoveredVehicles(),
driverResults, driverResults,
extractPartitionDebug(genericResult),
genericResult.notes(), genericResult.notes(),
genericResult.warnings() genericResult.warnings()
); );
} }
private static Map<String, RuntimeDriverPartitionDebugDto> extractPartitionDebug(
RuntimeEventProcessingResultDto genericResult
) {
LinkedHashMap<String, RuntimeDriverPartitionDebugDto> debugByDriver = new LinkedHashMap<>();
for (Map.Entry<String, RuntimeEventProcessingPartitionResultDto> entry : genericResult.partitionResults().entrySet()) {
Object debug = entry.getValue().metadata().get("partitionDebug");
if (debug instanceof RuntimeDriverPartitionDebugDto partitionDebug) {
debugByDriver.put(entry.getKey(), partitionDebug);
}
}
return debugByDriver;
}
} }

View File

@ -12,7 +12,8 @@ public record RuntimeEventPartitioningApiRequest(
Set<String> vehicleKeys, Set<String> vehicleKeys,
Boolean includeAllVehicles, Boolean includeAllVehicles,
Boolean attachVehicleEvidence, Boolean attachVehicleEvidence,
Integer vehicleEvidencePaddingMinutes Integer vehicleEvidencePaddingMinutes,
Boolean includeDebug
) { ) {
public RuntimeEventPartitioningApiRequest { public RuntimeEventPartitioningApiRequest {
strategy = strategy == null ? RuntimeEventPartitioningStrategy.CUSTOM_PROFILE : strategy; strategy = strategy == null ? RuntimeEventPartitioningStrategy.CUSTOM_PROFILE : strategy;
@ -43,4 +44,8 @@ public record RuntimeEventPartitioningApiRequest(
public int vehicleEvidencePaddingMinutesOrDefault(int fallback) { public int vehicleEvidencePaddingMinutesOrDefault(int fallback) {
return vehicleEvidencePaddingMinutes == null ? Math.max(0, fallback) : Math.max(0, vehicleEvidencePaddingMinutes); return vehicleEvidencePaddingMinutes == null ? Math.max(0, fallback) : Math.max(0, vehicleEvidencePaddingMinutes);
} }
public boolean includeDebugOrDefault() {
return includeDebug != null && includeDebug;
}
} }

View File

@ -20,7 +20,7 @@ public record RuntimeEventProcessingApiRequest(
throw new IllegalArgumentException("scope must not be null"); throw new IllegalArgumentException("scope must not be null");
} }
partitioning = partitioning == null partitioning = partitioning == null
? new RuntimeEventPartitioningApiRequest(null, null, null, null, null, null, null, null, null) ? new RuntimeEventPartitioningApiRequest(null, null, null, null, null, null, null, null, null, null)
: partitioning; : partitioning;
parameters = parameters == null parameters = parameters == null
? Map.of() ? Map.of()
@ -40,7 +40,8 @@ public record RuntimeEventProcessingApiRequest(
scope != null ? scope.vehicleKeys() : null, scope != null ? scope.vehicleKeys() : null,
scope != null ? scope.includeAllVehicles() : null, scope != null ? scope.includeAllVehicles() : null,
null, null,
scope != null ? scope.vehicleExpansionPaddingMinutes() : null scope != null ? scope.vehicleExpansionPaddingMinutes() : null,
null
), ),
Map.of() Map.of()
); );

View File

@ -55,30 +55,49 @@ public class TachographDriverEsperRuntimeEventProcessingProfile implements Runti
@Override @Override
public Set<String> optionalParameters() { public Set<String> optionalParameters() {
return Set.of("significantDrivingMinutes", "minimumRestPeriodMinutes", "attachVehicleOnlyEvents", "vehicleEvidencePaddingMinutes"); return Set.of(
"significantDrivingMinutes",
"minimumRestPeriodMinutes",
"attachVehicleOnlyEvents",
"vehicleEvidencePaddingMinutes",
"includePartitionDebug"
);
} }
@Override @Override
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) { public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
boolean includePartitionDebug = booleanParameter(
request.parameters(),
"includePartitionDebug",
request.partitioning() != null && request.partitioning().includeDebugOrDefault()
);
UnifiedRuntimeProcessingApiRequest tachographScopeRequest = applyGenericRequest(request.scope(), request.partitioning(), request.parameters()); UnifiedRuntimeProcessingApiRequest tachographScopeRequest = applyGenericRequest(request.scope(), request.partitioning(), request.parameters());
UnifiedRuntimeTachographEsperScopeResultDto tachographResult = tachographScopeProcessingService.processScope(tachographScopeRequest); UnifiedRuntimeTachographEsperScopeResultDto tachographResult = tachographScopeProcessingService.processScope(
tachographScopeRequest,
includePartitionDebug
);
Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults = new LinkedHashMap<>(); Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults = new LinkedHashMap<>();
tachographResult.driverResults().forEach((driverKey, driverResult) -> partitionResults.put( tachographResult.driverResults().forEach((driverKey, driverResult) -> {
driverKey, Map<String, Object> metadata = new LinkedHashMap<>();
new RuntimeEventProcessingPartitionResultDto( metadata.put("projectionResultType", driverResult.projection() == null ? "NONE" : "TachographEsperDriverProcessingResultDto");
"DRIVER", metadata.put("driverSeedEventCount", driverResult.driverSeedEventCount());
driverKey, metadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount());
"UnifiedRuntimeDerivedProjectionResultDto", metadata.put("mergedEventCount", driverResult.mergedEventCount());
driverResult, if (driverResult.partitionDebug() != null) {
Map.of( metadata.put("partitionDebug", driverResult.partitionDebug());
"projectionResultType", driverResult.projection() == null ? "NONE" : "TachographEsperDriverProcessingResultDto", }
"driverSeedEventCount", driverResult.driverSeedEventCount(), partitionResults.put(
"expandedVehicleEventCount", driverResult.expandedVehicleEventCount(), driverKey,
"mergedEventCount", driverResult.mergedEventCount() new RuntimeEventProcessingPartitionResultDto(
) "DRIVER",
) driverKey,
)); "UnifiedRuntimeDerivedProjectionResultDto",
driverResult,
metadata
)
);
});
return new RuntimeEventProcessingResultDto( return new RuntimeEventProcessingResultDto(
profileKey(), profileKey(),

View File

@ -1,6 +1,9 @@
package at.procon.eventhub.processing.model; package at.procon.eventhub.processing.model;
import at.procon.eventhub.dto.EventHubEventDto; import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleEvidenceAttachmentDecisionDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
import java.util.List; import java.util.List;
public record RuntimeDriverVehicleEvidenceAttachmentResult( public record RuntimeDriverVehicleEvidenceAttachmentResult(
@ -11,6 +14,8 @@ public record RuntimeDriverVehicleEvidenceAttachmentResult(
int vehicleUsageIntervalCount, int vehicleUsageIntervalCount,
int candidateVehicleEvidenceEventCount, int candidateVehicleEvidenceEventCount,
int ignoredVehicleEvidenceEventCount, int ignoredVehicleEvidenceEventCount,
List<RuntimeVehicleUsageIntervalDebugDto> vehicleUsageIntervals,
List<RuntimeVehicleEvidenceAttachmentDecisionDto> vehicleEvidenceDecisions,
List<String> notes, List<String> notes,
List<String> warnings List<String> warnings
) { ) {
@ -18,7 +23,25 @@ public record RuntimeDriverVehicleEvidenceAttachmentResult(
directDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents); directDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
attachedVehicleEvidenceEvents = attachedVehicleEvidenceEvents == null ? List.of() : List.copyOf(attachedVehicleEvidenceEvents); attachedVehicleEvidenceEvents = attachedVehicleEvidenceEvents == null ? List.of() : List.copyOf(attachedVehicleEvidenceEvents);
mergedEvents = mergedEvents == null ? List.of() : List.copyOf(mergedEvents); mergedEvents = mergedEvents == null ? List.of() : List.copyOf(mergedEvents);
vehicleUsageIntervals = vehicleUsageIntervals == null ? List.of() : List.copyOf(vehicleUsageIntervals);
vehicleEvidenceDecisions = vehicleEvidenceDecisions == null ? List.of() : List.copyOf(vehicleEvidenceDecisions);
notes = notes == null ? List.of() : List.copyOf(notes); notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings); warnings = warnings == null ? List.of() : List.copyOf(warnings);
} }
public RuntimeDriverPartitionDebugDto toPartitionDebug() {
return new RuntimeDriverPartitionDebugDto(
driverKey,
directDriverEvents.size(),
vehicleUsageIntervalCount,
candidateVehicleEvidenceEventCount,
attachedVehicleEvidenceEvents.size(),
ignoredVehicleEvidenceEventCount,
mergedEvents.size(),
vehicleUsageIntervals,
vehicleEvidenceDecisions,
notes,
warnings
);
}
} }

View File

@ -2,6 +2,8 @@ package at.procon.eventhub.processing.service;
import at.procon.eventhub.dto.EventHubEventDto; import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto; import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleEvidenceAttachmentDecisionDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeType; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeType;
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult; import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
@ -38,6 +40,24 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
List<EventHubEventDto> runtimeScopeEvents, List<EventHubEventDto> runtimeScopeEvents,
boolean attachVehicleOnlyEvents, boolean attachVehicleOnlyEvents,
int vehicleEvidencePaddingMinutes int vehicleEvidencePaddingMinutes
) {
return attachVehicleEvidence(
driverKey,
directDriverEvents,
runtimeScopeEvents,
attachVehicleOnlyEvents,
vehicleEvidencePaddingMinutes,
false
);
}
public RuntimeDriverVehicleEvidenceAttachmentResult attachVehicleEvidence(
String driverKey,
List<EventHubEventDto> directDriverEvents,
List<EventHubEventDto> runtimeScopeEvents,
boolean attachVehicleOnlyEvents,
int vehicleEvidencePaddingMinutes,
boolean includeDebug
) { ) {
List<EventHubEventDto> safeDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents); List<EventHubEventDto> safeDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
List<EventHubEventDto> safeScopeEvents = runtimeScopeEvents == null ? List.of() : List.copyOf(runtimeScopeEvents); List<EventHubEventDto> safeScopeEvents = runtimeScopeEvents == null ? List.of() : List.copyOf(runtimeScopeEvents);
@ -47,14 +67,19 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
List<String> warnings = new ArrayList<>(); List<String> warnings = new ArrayList<>();
if (!attachVehicleOnlyEvents) { if (!attachVehicleOnlyEvents) {
notes.add("Vehicle-only evidence attachment is disabled for driver partition " + driverKey + "."); notes.add("Vehicle-only evidence attachment is disabled for driver partition " + driverKey + ".");
List<RuntimeVehicleEvidenceAttachmentDecisionDto> disabledDecisions = includeDebug
? disabledDecisions(safeScopeEvents)
: List.of();
return new RuntimeDriverVehicleEvidenceAttachmentResult( return new RuntimeDriverVehicleEvidenceAttachmentResult(
driverKey, driverKey,
safeDriverEvents, safeDriverEvents,
List.of(), List.of(),
deduplicateAndSort(safeDriverEvents, List.of()), deduplicateAndSort(safeDriverEvents, List.of()),
0, 0,
0, disabledDecisions.size(),
0, disabledDecisions.size(),
List.of(),
disabledDecisions,
notes, notes,
warnings warnings
); );
@ -62,6 +87,12 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(null, driverKey, safeDriverEvents); ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(null, driverKey, safeDriverEvents);
List<ResolvedVehicleUsageInterval> usageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals()); List<ResolvedVehicleUsageInterval> usageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals());
List<RuntimeVehicleUsageIntervalDebugDto> usageIntervalDebug = includeDebug
? usageIntervals.stream().map(RuntimeVehicleUsageIntervalDebugDto::from).toList()
: List.of();
List<RuntimeVehicleEvidenceAttachmentDecisionDto> decisions = includeDebug
? directDriverDecisions(safeDriverEvents)
: new ArrayList<>();
List<EventHubEventDto> candidateVehicleEvidence = safeScopeEvents.stream() List<EventHubEventDto> candidateVehicleEvidence = safeScopeEvents.stream()
.filter(event -> scopeClassifier.classify(event) == RuntimeEventScopeType.VEHICLE_SCOPED) .filter(event -> scopeClassifier.classify(event) == RuntimeEventScopeType.VEHICLE_SCOPED)
.toList(); .toList();
@ -71,9 +102,25 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
List<ResolvedVehicleUsageInterval> matchingIntervals = matchingUsageIntervals(vehicleEvent, usageIntervals, paddingMinutes); List<ResolvedVehicleUsageInterval> matchingIntervals = matchingUsageIntervals(vehicleEvent, usageIntervals, paddingMinutes);
if (matchingIntervals.isEmpty()) { if (matchingIntervals.isEmpty()) {
ignored++; ignored++;
if (includeDebug) {
decisions.add(decision(
"IGNORED_NO_OVERLAPPING_VEHICLE_USAGE",
"Vehicle-scoped event did not overlap any reconstructed vehicle-usage interval for driver " + driverKey + ".",
vehicleEvent,
List.of()
));
}
continue; continue;
} }
attached.add(vehicleEvent); attached.add(vehicleEvent);
if (includeDebug) {
decisions.add(decision(
"ATTACHED_VEHICLE_EVIDENCE",
"Vehicle-scoped event overlapped driver vehicle usage interval(s).",
vehicleEvent,
matchingIntervals
));
}
if (matchingIntervals.size() > 1) { if (matchingIntervals.size() > 1) {
warnings.add("Vehicle-only event " + vehicleEvent.externalSourceEventId() warnings.add("Vehicle-only event " + vehicleEvent.externalSourceEventId()
+ " matched multiple vehicle-usage intervals for driver " + driverKey + " matched multiple vehicle-usage intervals for driver " + driverKey
@ -100,11 +147,61 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
usageIntervals.size(), usageIntervals.size(),
candidateVehicleEvidence.size(), candidateVehicleEvidence.size(),
ignored, ignored,
usageIntervalDebug,
includeDebug ? decisions : List.of(),
notes, notes,
warnings warnings
); );
} }
private List<RuntimeVehicleEvidenceAttachmentDecisionDto> disabledDecisions(List<EventHubEventDto> runtimeScopeEvents) {
return (runtimeScopeEvents == null ? List.<EventHubEventDto>of() : runtimeScopeEvents).stream()
.filter(event -> scopeClassifier.classify(event) == RuntimeEventScopeType.VEHICLE_SCOPED)
.map(event -> decision(
"IGNORED_ATTACHMENT_DISABLED",
"Vehicle evidence attachment was disabled for this partition.",
event,
List.of()
))
.toList();
}
private List<RuntimeVehicleEvidenceAttachmentDecisionDto> directDriverDecisions(List<EventHubEventDto> directDriverEvents) {
return (directDriverEvents == null ? List.<EventHubEventDto>of() : directDriverEvents).stream()
.map(event -> decision(
"DIRECT_DRIVER_EVENT",
"Event already carries the selected driver reference and belongs directly to the driver partition.",
event,
List.of()
))
.toList();
}
private RuntimeVehicleEvidenceAttachmentDecisionDto decision(
String decision,
String reason,
EventHubEventDto event,
List<ResolvedVehicleUsageInterval> matchingIntervals
) {
List<String> intervalIds = (matchingIntervals == null ? List.<ResolvedVehicleUsageInterval>of() : matchingIntervals).stream()
.map(ResolvedVehicleUsageInterval::intervalId)
.filter(Objects::nonNull)
.toList();
return new RuntimeVehicleEvidenceAttachmentDecisionDto(
decision,
reason,
dedupKey(event),
event == null ? null : event.externalSourceEventId(),
event == null ? null : event.occurredAt(),
event == null || event.eventDomain() == null ? null : event.eventDomain().name(),
event == null || event.eventType() == null ? null : event.eventType().name(),
event == null || event.lifecycle() == null ? null : event.lifecycle().name(),
scopeClassifier.classify(event),
vehicleKeys(event),
intervalIds
);
}
private List<ResolvedVehicleUsageInterval> matchingUsageIntervals( private List<ResolvedVehicleUsageInterval> matchingUsageIntervals(
EventHubEventDto vehicleEvent, EventHubEventDto vehicleEvent,
List<ResolvedVehicleUsageInterval> usageIntervals, List<ResolvedVehicleUsageInterval> usageIntervals,

View File

@ -3,6 +3,7 @@ package at.procon.eventhub.processing.service;
import at.procon.eventhub.dto.DriverRefDto; import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventHubEventDto; import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto; import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
@ -39,6 +40,13 @@ public class UnifiedRuntimeTachographEsperScopeProcessingService {
} }
public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) { public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) {
return processScope(apiRequest, false);
}
public UnifiedRuntimeTachographEsperScopeResultDto processScope(
UnifiedRuntimeProcessingApiRequest apiRequest,
boolean includePartitionDebug
) {
UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest(); UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest();
UnifiedRuntimeEventBundle broadBundle = eventAssemblyService.assembleDriverScopedEvents(request); UnifiedRuntimeEventBundle broadBundle = eventAssemblyService.assembleDriverScopedEvents(request);
LinkedHashSet<String> selectedDriverKeys = selectedDriverKeys(request, broadBundle.mergedEvents()); LinkedHashSet<String> selectedDriverKeys = selectedDriverKeys(request, broadBundle.mergedEvents());
@ -47,10 +55,15 @@ public class UnifiedRuntimeTachographEsperScopeProcessingService {
} }
Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults = new LinkedHashMap<>(); Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults = new LinkedHashMap<>();
Map<String, RuntimeDriverPartitionDebugDto> partitionDebugByDriver = new LinkedHashMap<>();
Map<String, List<String>> attachedVehicleEvidenceByEvent = new LinkedHashMap<>(); Map<String, List<String>> attachedVehicleEvidenceByEvent = new LinkedHashMap<>();
List<String> warnings = new ArrayList<>(); List<String> warnings = new ArrayList<>();
for (String driverKey : selectedDriverKeys) { for (String driverKey : selectedDriverKeys) {
UnifiedRuntimeEventBundle driverBundle = partitionForDriver(request, broadBundle, driverKey); DriverPartition driverPartition = partitionForDriver(request, broadBundle, driverKey, includePartitionDebug);
UnifiedRuntimeEventBundle driverBundle = driverPartition.bundle();
if (driverPartition.debug() != null) {
partitionDebugByDriver.put(driverKey, driverPartition.debug());
}
for (EventHubEventDto attachedEvent : driverBundle.expandedVehicleEvents()) { for (EventHubEventDto attachedEvent : driverBundle.expandedVehicleEvents()) {
attachedVehicleEvidenceByEvent attachedVehicleEvidenceByEvent
.computeIfAbsent(dedupKey(attachedEvent), ignored -> new ArrayList<>()) .computeIfAbsent(dedupKey(attachedEvent), ignored -> new ArrayList<>())
@ -71,7 +84,7 @@ public class UnifiedRuntimeTachographEsperScopeProcessingService {
driverBundle, driverBundle,
driverKey driverKey
); );
driverResults.put(driverKey, driverResult); driverResults.put(driverKey, driverPartition.debug() == null ? driverResult : driverResult.withPartitionDebug(driverPartition.debug()));
} }
attachedVehicleEvidenceByEvent.forEach((eventKey, drivers) -> { attachedVehicleEvidenceByEvent.forEach((eventKey, drivers) -> {
if (drivers.size() > 1) { if (drivers.size() > 1) {
@ -98,6 +111,7 @@ public class UnifiedRuntimeTachographEsperScopeProcessingService {
broadBundle.discoveredVehicles().size(), broadBundle.discoveredVehicles().size(),
broadBundle.discoveredVehicles(), broadBundle.discoveredVehicles(),
driverResults, driverResults,
partitionDebugByDriver,
notes, notes,
warnings warnings
); );
@ -130,10 +144,11 @@ public class UnifiedRuntimeTachographEsperScopeProcessingService {
return selected; return selected;
} }
private UnifiedRuntimeEventBundle partitionForDriver( private DriverPartition partitionForDriver(
UnifiedRuntimeProcessingRequest request, UnifiedRuntimeProcessingRequest request,
UnifiedRuntimeEventBundle broadBundle, UnifiedRuntimeEventBundle broadBundle,
String driverKey String driverKey,
boolean includePartitionDebug
) { ) {
List<EventHubEventDto> directDriverEvents = broadBundle.mergedEvents().stream() List<EventHubEventDto> directDriverEvents = broadBundle.mergedEvents().stream()
.filter(event -> Objects.equals(driverKey(event), driverKey)) .filter(event -> Objects.equals(driverKey(event), driverKey))
@ -143,7 +158,8 @@ public class UnifiedRuntimeTachographEsperScopeProcessingService {
directDriverEvents, directDriverEvents,
broadBundle.mergedEvents(), broadBundle.mergedEvents(),
request.expandVehicleEvents(), request.expandVehicleEvents(),
request.vehicleExpansionPaddingMinutes() request.vehicleExpansionPaddingMinutes(),
includePartitionDebug
); );
List<UnifiedDiscoveredVehicleRef> driverVehicles = discoverVehicles(attachmentResult.mergedEvents()); List<UnifiedDiscoveredVehicleRef> driverVehicles = discoverVehicles(attachmentResult.mergedEvents());
List<String> notes = new ArrayList<>(broadBundle.notes()); List<String> notes = new ArrayList<>(broadBundle.notes());
@ -153,7 +169,7 @@ public class UnifiedRuntimeTachographEsperScopeProcessingService {
notes.add("Vehicle-usage intervals used for temporal evidence attachment: " + attachmentResult.vehicleUsageIntervalCount() + "."); notes.add("Vehicle-usage intervals used for temporal evidence attachment: " + attachmentResult.vehicleUsageIntervalCount() + ".");
notes.addAll(attachmentResult.notes()); notes.addAll(attachmentResult.notes());
attachmentResult.warnings().forEach(warning -> notes.add("WARNING: " + warning)); attachmentResult.warnings().forEach(warning -> notes.add("WARNING: " + warning));
return new UnifiedRuntimeEventBundle( UnifiedRuntimeEventBundle bundle = new UnifiedRuntimeEventBundle(
request.withDriverKey(driverKey), request.withDriverKey(driverKey),
attachmentResult.directDriverEvents(), attachmentResult.directDriverEvents(),
driverVehicles, driverVehicles,
@ -161,6 +177,16 @@ public class UnifiedRuntimeTachographEsperScopeProcessingService {
attachmentResult.mergedEvents(), attachmentResult.mergedEvents(),
notes notes
); );
return new DriverPartition(
bundle,
includePartitionDebug ? attachmentResult.toPartitionDebug() : null
);
}
private record DriverPartition(
UnifiedRuntimeEventBundle bundle,
RuntimeDriverPartitionDebugDto debug
) {
} }
private LinkedHashSet<String> discoverDriverKeys(List<EventHubEventDto> events) { private LinkedHashSet<String> discoverDriverKeys(List<EventHubEventDto> events) {

View File

@ -50,7 +50,7 @@ class RuntimeEventProcessingServiceTest {
null, null,
null null
), ),
new RuntimeEventPartitioningApiRequest(RuntimeEventPartitioningStrategy.DRIVER, null, false, null, false, null, false, null, null), new RuntimeEventPartitioningApiRequest(RuntimeEventPartitioningStrategy.DRIVER, null, false, null, false, null, false, null, null, null),
Map.of() Map.of()
); );

View File

@ -2,6 +2,7 @@ package at.procon.eventhub.processing.eventprocessing.profile;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -34,7 +35,13 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
assertThat(profile.displayName()).isEqualTo("Tachograph Driver Esper Processing"); assertThat(profile.displayName()).isEqualTo("Tachograph Driver Esper Processing");
assertThat(profile.defaultPartitioningStrategy()).isEqualTo(RuntimeEventPartitioningStrategy.DRIVER); assertThat(profile.defaultPartitioningStrategy()).isEqualTo(RuntimeEventPartitioningStrategy.DRIVER);
assertThat(profile.supportedPartitioningStrategies()).containsExactly(RuntimeEventPartitioningStrategy.DRIVER); assertThat(profile.supportedPartitioningStrategies()).containsExactly(RuntimeEventPartitioningStrategy.DRIVER);
assertThat(profile.optionalParameters()).containsExactlyInAnyOrder("significantDrivingMinutes", "minimumRestPeriodMinutes", "attachVehicleOnlyEvents", "vehicleEvidencePaddingMinutes"); assertThat(profile.optionalParameters()).containsExactlyInAnyOrder(
"significantDrivingMinutes",
"minimumRestPeriodMinutes",
"attachVehicleOnlyEvents",
"vehicleEvidencePaddingMinutes",
"includePartitionDebug"
);
} }
@Test @Test
@ -77,13 +84,15 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
null, null,
null, null,
null, null,
null,
null null
), ),
Map.of( Map.of(
"significantDrivingMinutes", 5, "significantDrivingMinutes", 5,
"minimumRestPeriodMinutes", "600", "minimumRestPeriodMinutes", "600",
"vehicleEvidencePaddingMinutes", 20, "vehicleEvidencePaddingMinutes", 20,
"attachVehicleOnlyEvents", true "attachVehicleOnlyEvents", true,
"includePartitionDebug", true
) )
); );
@ -117,7 +126,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
null, null,
List.of("driver processed") List.of("driver processed")
); );
when(scopeService.processScope(any())) when(scopeService.processScope(any(), anyBoolean()))
.thenReturn(new UnifiedRuntimeTachographEsperScopeResultDto( .thenReturn(new UnifiedRuntimeTachographEsperScopeResultDto(
processedRequest, processedRequest,
5, 5,
@ -125,6 +134,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
1, 1,
List.of(), List.of(),
Map.of("12:DRIVER-1", driverResult), Map.of("12:DRIVER-1", driverResult),
Map.of(),
List.of("scope processed"), List.of("scope processed"),
List.of() List.of()
)); ));
@ -138,12 +148,14 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
assertThat(result.partitionResults().get("12:DRIVER-1").result()).isSameAs(driverResult); assertThat(result.partitionResults().get("12:DRIVER-1").result()).isSameAs(driverResult);
ArgumentCaptor<UnifiedRuntimeProcessingApiRequest> captor = ArgumentCaptor.forClass(UnifiedRuntimeProcessingApiRequest.class); ArgumentCaptor<UnifiedRuntimeProcessingApiRequest> captor = ArgumentCaptor.forClass(UnifiedRuntimeProcessingApiRequest.class);
verify(scopeService).processScope(captor.capture()); ArgumentCaptor<Boolean> debugCaptor = ArgumentCaptor.forClass(Boolean.class);
verify(scopeService).processScope(captor.capture(), debugCaptor.capture());
UnifiedRuntimeProcessingApiRequest delegated = captor.getValue(); UnifiedRuntimeProcessingApiRequest delegated = captor.getValue();
assertThat(delegated.driverKeys()).containsExactly("12:DRIVER-1"); assertThat(delegated.driverKeys()).containsExactly("12:DRIVER-1");
assertThat(delegated.significantDrivingMinutes()).isEqualTo(5); assertThat(delegated.significantDrivingMinutes()).isEqualTo(5);
assertThat(delegated.minimumRestPeriodMinutes()).isEqualTo(600); assertThat(delegated.minimumRestPeriodMinutes()).isEqualTo(600);
assertThat(delegated.vehicleExpansionPaddingMinutes()).isEqualTo(20); assertThat(delegated.vehicleExpansionPaddingMinutes()).isEqualTo(20);
assertThat(delegated.expandVehicleEvents()).isTrue(); assertThat(delegated.expandVehicleEvents()).isTrue();
assertThat(debugCaptor.getValue()).isTrue();
} }
} }

View File

@ -9,6 +9,7 @@ import at.procon.eventhub.dto.EventLifecycle;
import at.procon.eventhub.dto.EventType; import at.procon.eventhub.dto.EventType;
import at.procon.eventhub.dto.VehicleRefDto; import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.dto.VehicleRegistrationRefDto; import at.procon.eventhub.dto.VehicleRegistrationRefDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleEvidenceAttachmentDecisionDto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier;
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult; import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@ -119,6 +120,34 @@ class RuntimeDriverVehicleEvidenceAttachmentServiceTest {
.containsExactly("pos-midnight"); .containsExactly("pos-midnight");
} }
@Test
void producesDebugDecisionsForDirectAttachedAndIgnoredEvents() {
List<EventHubEventDto> driverEvents = List.of(
cardEvent("card-1-in", EventType.CARD_INSERTED, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T08:00:00Z"),
cardEvent("card-1-out", EventType.CARD_WITHDRAWN, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T18:00:00Z")
);
EventHubEventDto inside = vehicleOnlyEvent("pos-inside", "VIN-1", "AT:W-1", "2026-05-01T12:00:00Z");
EventHubEventDto outside = vehicleOnlyEvent("pos-outside", "VIN-1", "AT:W-1", "2026-05-01T20:00:00Z");
RuntimeDriverVehicleEvidenceAttachmentResult result = service.attachVehicleEvidence(
"DRIVER-1",
driverEvents,
List.of(driverEvents.get(0), driverEvents.get(1), inside, outside),
true,
0,
true
);
assertThat(result.vehicleUsageIntervals()).hasSize(1);
assertThat(result.vehicleEvidenceDecisions()).extracting(RuntimeVehicleEvidenceAttachmentDecisionDto::decision)
.contains(
"DIRECT_DRIVER_EVENT",
"ATTACHED_VEHICLE_EVIDENCE",
"IGNORED_NO_OVERLAPPING_VEHICLE_USAGE"
);
assertThat(result.toPartitionDebug().vehicleEvidenceDecisions()).hasSameSizeAs(result.vehicleEvidenceDecisions());
}
private EventHubEventDto cardEvent( private EventHubEventDto cardEvent(
String externalId, String externalId,
EventType eventType, EventType eventType,