Adjust runtime interval loading and result projections

This commit is contained in:
trifonovt 2026-06-03 17:19:21 +02:00
parent 7457872d51
commit 6bef8becf9
31 changed files with 1013 additions and 95 deletions

View File

@ -55,6 +55,7 @@ public class EventHubEventReadRepository {
request.tenantKey(),
request.occurredFrom(),
request.occurredTo(),
request.includeIntersectingIntervals() && "TACHOGRAPH".equalsIgnoreCase(providerKey),
request.driverSourceEntityId(),
request.driverCardNation(),
request.driverCardNumber(),
@ -76,6 +77,7 @@ public class EventHubEventReadRepository {
request.tenantKey(),
request.occurredFrom(),
request.occurredTo(),
request.includeIntersectingIntervals() && "TACHOGRAPH".equalsIgnoreCase(providerKey),
null,
null,
null,
@ -92,6 +94,7 @@ public class EventHubEventReadRepository {
String tenantKey,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals,
String driverSourceEntityId,
String driverCardNation,
String driverCardNumber,
@ -104,6 +107,7 @@ public class EventHubEventReadRepository {
) {
StringBuilder sql = new StringBuilder(
"""
with candidate_events as (
select
event.id,
event.external_source_event_id,
@ -223,14 +227,6 @@ public class EventHubEventReadRepository {
}
sql.append(")");
}
if (occurredFrom != null) {
sql.append(" and event.occurred_at >= ?");
params.add(occurredFrom);
}
if (occurredTo != null) {
sql.append(" and event.occurred_at <= ?");
params.add(occurredTo);
}
if (driverSourceEntityId != null) {
sql.append(
"""
@ -307,7 +303,9 @@ public class EventHubEventReadRepository {
}
}
sql.append(" order by event.occurred_at, event.event_domain, event.event_type, event.lifecycle, event.id");
sql.append("\n)");
appendTemporalFilter(sql, params, occurredFrom, occurredTo, includeIntersectingIntervals);
sql.append(" order by occurred_at, event_domain, event_type, lifecycle, id");
return jdbcTemplate.query(
sql.toString(),
@ -316,6 +314,109 @@ public class EventHubEventReadRepository {
);
}
private void appendTemporalFilter(
StringBuilder sql,
List<Object> params,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals
) {
if (!includeIntersectingIntervals) {
sql.append("\nselect * from candidate_events");
appendPointWindowFilter(sql, params, occurredFrom, occurredTo);
return;
}
if (occurredFrom == null && occurredTo == null) {
sql.append("\nselect * from candidate_events");
return;
}
sql.append(
"""
, interval_events as (
select
'DRIVER_ACTIVITY' as interval_scope,
coalesce(payload #>> '{raw,intervalId}', payload #>> '{raw,sourceRowId}', external_source_event_id) as interval_key,
min(case when lifecycle = 'START' then occurred_at end) as started_at,
max(case when lifecycle = 'END' then occurred_at end) as ended_at
from candidate_events
where event_domain = 'DRIVER_ACTIVITY'
group by 1, 2
union all
select
'DRIVER_CARD' as interval_scope,
coalesce(payload #>> '{raw,intervalId}', payload #>> '{raw,sourceRowId}', external_source_event_id) as interval_key,
min(case when event_type = 'CARD_INSERTED' then occurred_at end) as started_at,
max(case when event_type = 'CARD_WITHDRAWN' then occurred_at end) as ended_at
from candidate_events
where event_domain = 'DRIVER_CARD'
and event_type in ('CARD_INSERTED', 'CARD_WITHDRAWN')
group by 1, 2
)
select ce.*
from candidate_events ce
left join interval_events ie
on ie.interval_key = coalesce(ce.payload #>> '{raw,intervalId}', ce.payload #>> '{raw,sourceRowId}', ce.external_source_event_id)
and (
(ie.interval_scope = 'DRIVER_ACTIVITY' and ce.event_domain = 'DRIVER_ACTIVITY')
or (ie.interval_scope = 'DRIVER_CARD'
and ce.event_domain = 'DRIVER_CARD'
and ce.event_type in ('CARD_INSERTED', 'CARD_WITHDRAWN'))
)
"""
);
StringBuilder where = new StringBuilder();
appendPointWindowPredicate(where, params, "ce.occurred_at", occurredFrom, occurredTo);
if (where.length() == 0) {
where.append("\nwhere 1 = 0");
}
where.append("\n or (ie.interval_key is not null");
if (occurredTo != null) {
where.append("\n and ie.started_at <= ?");
params.add(occurredTo);
}
if (occurredFrom != null) {
where.append("\n and coalesce(ie.ended_at, 'infinity'::timestamptz) >= ?");
params.add(occurredFrom);
}
where.append("\n )");
sql.append(where);
}
private void appendPointWindowFilter(
StringBuilder sql,
List<Object> params,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
) {
StringBuilder where = new StringBuilder();
appendPointWindowPredicate(where, params, "occurred_at", occurredFrom, occurredTo);
sql.append(where);
}
private void appendPointWindowPredicate(
StringBuilder sql,
List<Object> params,
String occurredAtColumn,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
) {
boolean hasCondition = false;
if (occurredFrom != null) {
sql.append(hasCondition ? " and " : "\nwhere ");
sql.append(occurredAtColumn).append(" >= ?");
params.add(occurredFrom);
hasCondition = true;
}
if (occurredTo != null) {
sql.append(hasCondition ? " and " : "\nwhere ");
sql.append(occurredAtColumn).append(" <= ?");
params.add(occurredTo);
}
}
private EventHubEventDto mapEvent(ResultSet rs) throws SQLException {
DriverRefDto driverRef = driverRef(rs);
VehicleRefDto vehicleRef = vehicleRef(rs);

View File

@ -47,4 +47,48 @@ public record DriverWorkingTimeProcessingResultDto(
List<DriverWorkingTimeSupportGeoEvent> supportGeoEvents,
List<String> notes
) {
public DriverWorkingTimeProcessingResultDto withIncludedIntervals(
boolean includeActivityIntervals,
boolean includeDrivingIntervals
) {
if (includeActivityIntervals && includeDrivingIntervals) {
return this;
}
return new DriverWorkingTimeProcessingResultDto(
sessionId,
driverKey,
sourceKind,
loadedFrom,
loadedTo,
requestedFrom,
requestedTo,
activityIntervalCount,
drivingIntervalCount,
drivingInterruptionIntervalCount,
drivingInterruptionVehicleChangeIntervalCount,
dailyWeeklyRestCandidateIntervalCount,
dailyWeeklyRestCandidateCoverageIntervalCount,
unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
potentialHomeOvernightStayIntervalCount,
potentialInVehicleOvernightStayIntervalCount,
potentialInVehicleTripIntervalCount,
vehicleUsageIntervalCount,
vuCardAbsentIntervalCount,
supportGeoEventCount,
includeActivityIntervals ? activityIntervals : List.of(),
includeDrivingIntervals ? drivingIntervals : List.of(),
drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals,
dailyWeeklyRestCandidateIntervals,
dailyWeeklyRestCandidateCoverageIntervals,
unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
potentialHomeOvernightStayIntervals,
potentialInVehicleOvernightStayIntervals,
potentialInVehicleTripIntervals,
vehicleUsageIntervals,
vuCardAbsentIntervals,
supportGeoEvents,
notes
);
}
}

View File

@ -29,8 +29,11 @@ public record UnifiedRuntimeProcessingApiRequest(
OffsetDateTime occurredTo,
Boolean expandVehicleEvents,
Integer vehicleExpansionPaddingMinutes,
Boolean includeIntersectingIntervals,
Integer significantDrivingMinutes,
Integer minimumRestPeriodMinutes
Integer minimumRestPeriodMinutes,
Boolean includeActivityIntervals,
Boolean includeDrivingIntervals
) {
public UnifiedRuntimeProcessingRequest toRuntimeRequest() {
return new UnifiedRuntimeProcessingRequest(
@ -52,7 +55,16 @@ public record UnifiedRuntimeProcessingApiRequest(
occurredFrom,
occurredTo,
expandVehicleEvents == null || expandVehicleEvents,
vehicleExpansionPaddingMinutes == null ? 0 : Math.max(0, vehicleExpansionPaddingMinutes)
vehicleExpansionPaddingMinutes == null ? 0 : Math.max(0, vehicleExpansionPaddingMinutes),
includeIntersectingIntervals == null || includeIntersectingIntervals
);
}
public boolean includeActivityIntervalsOrDefault() {
return includeActivityIntervals != null && includeActivityIntervals;
}
public boolean includeDrivingIntervalsOrDefault() {
return includeDrivingIntervals != null && includeDrivingIntervals;
}
}

View File

@ -69,7 +69,11 @@ public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcess
for (Map.Entry<String, DriverWorkingTimePreparedInput> entry : preparedInputs.entrySet()) {
DriverWorkingTimePreparedInput preparedInput = entry.getValue();
DriverWorkingTimeProcessingResultDto projection =
workingTimeProcessingCore.process(preparedInput.processingInput());
workingTimeProcessingCore.process(preparedInput.processingInput())
.withIncludedIntervals(
scopeRequest.includeActivityIntervalsOrDefault(),
scopeRequest.includeDrivingIntervalsOrDefault()
);
warnings.addAll(preparedInput.partition().warnings());
UnifiedRuntimeProcessingRequest driverRequest = broadBundle.request().withDriverKey(preparedInput.driverKey());
driverResults.put(preparedInput.driverKey(), new UnifiedRuntimeDerivedProjectionResultDto(

View File

@ -168,6 +168,8 @@ public class DriverWorkingTimeRuntimeProcessingPlan implements RuntimeProcessing
"minimumRestPeriodMinutes",
"attachVehicleOnlyEvents",
"vehicleEvidencePaddingMinutes",
"includeActivityIntervals",
"includeDrivingIntervals",
"includePartitionDebug"
);
}
@ -368,6 +370,10 @@ public class DriverWorkingTimeRuntimeProcessingPlan implements RuntimeProcessing
Integer significantDrivingMinutes = integerParameter(parameters, "significantDrivingMinutes", sourceSelection.significantDrivingMinutes());
Integer minimumRestPeriodMinutes = integerParameter(parameters, "minimumRestPeriodMinutes", sourceSelection.minimumRestPeriodMinutes());
boolean includeActivityIntervals = booleanParameter(parameters, "includeActivityIntervals",
sourceSelection.includeActivityIntervalsOrDefault());
boolean includeDrivingIntervals = booleanParameter(parameters, "includeDrivingIntervals",
sourceSelection.includeDrivingIntervalsOrDefault());
boolean attachVehicleOnlyEvents = booleanParameter(parameters, "attachVehicleOnlyEvents",
partitioning == null ? sourceSelection.expandVehicleEvents() == null || sourceSelection.expandVehicleEvents() : partitioning.attachVehicleEvidenceOrDefault());
Integer vehicleEvidencePaddingMinutes = nonNegativeIntegerParameter(parameters, "vehicleEvidencePaddingMinutes",
@ -395,8 +401,11 @@ public class DriverWorkingTimeRuntimeProcessingPlan implements RuntimeProcessing
sourceSelection.occurredTo(),
attachVehicleOnlyEvents,
vehicleEvidencePaddingMinutes,
sourceSelection.includeIntersectingIntervals(),
significantDrivingMinutes,
minimumRestPeriodMinutes
minimumRestPeriodMinutes,
includeActivityIntervals,
includeDrivingIntervals
);
}

View File

@ -178,8 +178,11 @@ public class RuntimeTachographParityValidationService {
request.occurredTo(),
request.expandVehicleEventsOrDefault(),
request.vehicleExpansionPaddingMinutesOrDefault(),
null,
request.significantDrivingMinutes(),
request.minimumRestPeriodMinutes()
request.minimumRestPeriodMinutes(),
false,
false
);
RuntimeEventPartitioningApiRequest partitioning = new RuntimeEventPartitioningApiRequest(
RuntimeEventPartitioningStrategy.DRIVER,

View File

@ -20,7 +20,8 @@ public record UnifiedDriverEventsRequest(
String registrationNation,
String registrationNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals
) {
public UnifiedDriverEventsRequest {
Objects.requireNonNull(sourceFamily, "sourceFamily must not be null");
@ -59,6 +60,16 @@ public record UnifiedDriverEventsRequest(
String driverKey,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
) {
return forTachographFileSession(sessionId, driverKey, occurredFrom, occurredTo, false);
}
public static UnifiedDriverEventsRequest forTachographFileSession(
UUID sessionId,
String driverKey,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals
) {
return new UnifiedDriverEventsRequest(
UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION,
@ -74,7 +85,8 @@ public record UnifiedDriverEventsRequest(
null,
null,
occurredFrom,
occurredTo
occurredTo,
includeIntersectingIntervals
);
}
@ -93,7 +105,29 @@ public record UnifiedDriverEventsRequest(
driverCardNumber,
occurredFrom,
occurredTo,
List.of()
List.of(),
false
);
}
public static UnifiedDriverEventsRequest forTachographDbDriver(
String tenantKey,
String driverSourceEntityId,
String driverCardNation,
String driverCardNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals
) {
return forTachographDbDriver(
tenantKey,
driverSourceEntityId,
driverCardNation,
driverCardNumber,
occurredFrom,
occurredTo,
List.of(),
includeIntersectingIntervals
);
}
@ -105,6 +139,28 @@ public record UnifiedDriverEventsRequest(
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
List<String> sourceKinds
) {
return forTachographDbDriver(
tenantKey,
driverSourceEntityId,
driverCardNation,
driverCardNumber,
occurredFrom,
occurredTo,
sourceKinds,
false
);
}
public static UnifiedDriverEventsRequest forTachographDbDriver(
String tenantKey,
String driverSourceEntityId,
String driverCardNation,
String driverCardNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
List<String> sourceKinds,
boolean includeIntersectingIntervals
) {
return new UnifiedDriverEventsRequest(
UnifiedEventSourceFamily.TACHOGRAPH_DB,
@ -120,7 +176,8 @@ public record UnifiedDriverEventsRequest(
null,
null,
occurredFrom,
occurredTo
occurredTo,
includeIntersectingIntervals
);
}
@ -147,7 +204,8 @@ public record UnifiedDriverEventsRequest(
registrationNation,
registrationNumber,
occurredFrom,
occurredTo
occurredTo,
false
);
}
@ -173,7 +231,8 @@ public record UnifiedDriverEventsRequest(
null,
null,
occurredFrom,
occurredTo
occurredTo,
false
);
}

View File

@ -28,7 +28,8 @@ public record UnifiedRuntimeProcessingRequest(
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
boolean expandVehicleEvents,
int vehicleExpansionPaddingMinutes
int vehicleExpansionPaddingMinutes,
boolean includeIntersectingIntervals
) {
public UnifiedRuntimeProcessingRequest {
if (sourceFamilies == null || sourceFamilies.isEmpty()) {
@ -118,7 +119,8 @@ public record UnifiedRuntimeProcessingRequest(
occurredTo,
UnifiedRuntimeEventBackend.SOURCE_DB,
true,
0
0,
true
);
}
@ -143,7 +145,8 @@ public record UnifiedRuntimeProcessingRequest(
occurredTo,
UnifiedRuntimeEventBackend.SOURCE_DB,
expandVehicleEvents,
vehicleExpansionPaddingMinutes
vehicleExpansionPaddingMinutes,
true
);
}
@ -158,6 +161,34 @@ public record UnifiedRuntimeProcessingRequest(
UnifiedRuntimeEventBackend eventBackend,
boolean expandVehicleEvents,
int vehicleExpansionPaddingMinutes
) {
return forDriver(
tenantKey,
sourceFamilies,
driverSourceEntityId,
driverCardNation,
driverCardNumber,
occurredFrom,
occurredTo,
eventBackend,
expandVehicleEvents,
vehicleExpansionPaddingMinutes,
true
);
}
public static UnifiedRuntimeProcessingRequest forDriver(
String tenantKey,
Set<UnifiedEventSourceFamily> sourceFamilies,
String driverSourceEntityId,
String driverCardNation,
String driverCardNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
UnifiedRuntimeEventBackend eventBackend,
boolean expandVehicleEvents,
int vehicleExpansionPaddingMinutes,
boolean includeIntersectingIntervals
) {
return new UnifiedRuntimeProcessingRequest(
null,
@ -178,7 +209,8 @@ public record UnifiedRuntimeProcessingRequest(
occurredFrom,
occurredTo,
expandVehicleEvents,
vehicleExpansionPaddingMinutes
vehicleExpansionPaddingMinutes,
includeIntersectingIntervals
);
}
@ -212,7 +244,8 @@ public record UnifiedRuntimeProcessingRequest(
occurredFrom,
occurredTo,
expandVehicleEvents,
vehicleExpansionPaddingMinutes
vehicleExpansionPaddingMinutes,
true
);
}
@ -243,7 +276,8 @@ public record UnifiedRuntimeProcessingRequest(
occurredFrom,
occurredTo,
expandVehicleEvents,
vehicleExpansionPaddingMinutes
vehicleExpansionPaddingMinutes,
true
);
}
@ -274,7 +308,8 @@ public record UnifiedRuntimeProcessingRequest(
occurredFrom,
occurredTo,
expandVehicleEvents,
vehicleExpansionPaddingMinutes
vehicleExpansionPaddingMinutes,
true
);
}
@ -305,7 +340,8 @@ public record UnifiedRuntimeProcessingRequest(
occurredFrom,
occurredTo,
expandVehicleEvents,
vehicleExpansionPaddingMinutes
vehicleExpansionPaddingMinutes,
true
);
}
@ -348,7 +384,8 @@ public record UnifiedRuntimeProcessingRequest(
occurredFrom,
occurredTo,
expandVehicleEvents,
vehicleExpansionPaddingMinutes
vehicleExpansionPaddingMinutes,
includeIntersectingIntervals
);
}

View File

@ -15,7 +15,8 @@ public record UnifiedVehicleEventsRequest(
String registrationNation,
String registrationNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals
) {
public UnifiedVehicleEventsRequest {
Objects.requireNonNull(sourceFamily, "sourceFamily must not be null");
@ -46,6 +47,28 @@ public record UnifiedVehicleEventsRequest(
String registrationNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
) {
return forTachographFileSession(
sessionId,
vehicleSourceEntityId,
vin,
registrationNation,
registrationNumber,
occurredFrom,
occurredTo,
false
);
}
public static UnifiedVehicleEventsRequest forTachographFileSession(
UUID sessionId,
String vehicleSourceEntityId,
String vin,
String registrationNation,
String registrationNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals
) {
return new UnifiedVehicleEventsRequest(
UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION,
@ -57,7 +80,8 @@ public record UnifiedVehicleEventsRequest(
registrationNation,
registrationNumber,
occurredFrom,
occurredTo
occurredTo,
includeIntersectingIntervals
);
}
@ -78,7 +102,31 @@ public record UnifiedVehicleEventsRequest(
registrationNumber,
occurredFrom,
occurredTo,
List.of()
List.of(),
false
);
}
public static UnifiedVehicleEventsRequest forTachographDb(
String tenantKey,
String vehicleSourceEntityId,
String vin,
String registrationNation,
String registrationNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals
) {
return forTachographDb(
tenantKey,
vehicleSourceEntityId,
vin,
registrationNation,
registrationNumber,
occurredFrom,
occurredTo,
List.of(),
includeIntersectingIntervals
);
}
@ -91,6 +139,30 @@ public record UnifiedVehicleEventsRequest(
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
List<String> sourceKinds
) {
return forTachographDb(
tenantKey,
vehicleSourceEntityId,
vin,
registrationNation,
registrationNumber,
occurredFrom,
occurredTo,
sourceKinds,
false
);
}
public static UnifiedVehicleEventsRequest forTachographDb(
String tenantKey,
String vehicleSourceEntityId,
String vin,
String registrationNation,
String registrationNumber,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
List<String> sourceKinds,
boolean includeIntersectingIntervals
) {
return new UnifiedVehicleEventsRequest(
UnifiedEventSourceFamily.TACHOGRAPH_DB,
@ -102,7 +174,8 @@ public record UnifiedVehicleEventsRequest(
registrationNation,
registrationNumber,
occurredFrom,
occurredTo
occurredTo,
includeIntersectingIntervals
);
}
@ -125,7 +198,8 @@ public record UnifiedVehicleEventsRequest(
registrationNation,
registrationNumber,
occurredFrom,
occurredTo
occurredTo,
false
);
}

View File

@ -71,7 +71,8 @@ public class EventHubRuntimeEventLoader implements RuntimeDriverEventLoader, Run
request.driverCardNumber(),
request.occurredFrom(),
request.occurredTo(),
request.tachographSourceKindNames()
request.tachographSourceKindNames(),
request.includeIntersectingIntervals()
);
case YELLOWFOX_DB -> UnifiedDriverEventsRequest.forYellowFoxDbDriver(
request.tenantKey(),
@ -99,7 +100,8 @@ public class EventHubRuntimeEventLoader implements RuntimeDriverEventLoader, Run
vehicleRef.registrationNumber(),
request.vehicleOccurredFrom(),
request.vehicleOccurredTo(),
request.tachographSourceKindNames()
request.tachographSourceKindNames(),
request.includeIntersectingIntervals()
);
case YELLOWFOX_DB -> UnifiedVehicleEventsRequest.forYellowFoxDb(
request.tenantKey(),

View File

@ -0,0 +1,180 @@
package at.procon.eventhub.processing.service;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.EventLifecycle;
import at.procon.eventhub.dto.EventType;
import com.fasterxml.jackson.databind.JsonNode;
import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
final class RuntimeIntervalEventWindowSelector {
private RuntimeIntervalEventWindowSelector() {
}
static TachographTimelineEventBundle filterBundle(
TachographTimelineEventBundle bundle,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals
) {
if (bundle == null) {
return new TachographTimelineEventBundle(List.of(), List.of(), List.of());
}
return new TachographTimelineEventBundle(
filterIntervalEvents(bundle.activityEvents(), occurredFrom, occurredTo, includeIntersectingIntervals),
filterIntervalEvents(bundle.vehicleUsageEvents(), occurredFrom, occurredTo, includeIntersectingIntervals),
filterPointEvents(bundle.supportEvents(), occurredFrom, occurredTo)
);
}
private static List<EventHubEventDto> filterIntervalEvents(
List<EventHubEventDto> events,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
boolean includeIntersectingIntervals
) {
if (events == null || events.isEmpty()) {
return List.of();
}
if (!includeIntersectingIntervals) {
return filterPointEvents(events, occurredFrom, occurredTo);
}
LinkedHashMap<String, IntervalGroup> byInterval = new LinkedHashMap<>();
for (EventHubEventDto event : events) {
byInterval.computeIfAbsent(intervalKey(event), ignored -> new IntervalGroup()).add(event);
}
List<EventHubEventDto> result = new ArrayList<>();
for (IntervalGroup group : byInterval.values()) {
if (group.overlaps(occurredFrom, occurredTo)) {
result.addAll(group.events);
}
}
return List.copyOf(result);
}
private static List<EventHubEventDto> filterPointEvents(
List<EventHubEventDto> events,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
) {
return events.stream()
.filter(event -> withinWindow(event.occurredAt(), occurredFrom, occurredTo))
.toList();
}
private static boolean withinWindow(
OffsetDateTime occurredAt,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
) {
if (occurredAt == null) {
return false;
}
if (occurredFrom != null && occurredAt.isBefore(occurredFrom)) {
return false;
}
return occurredTo == null || !occurredAt.isAfter(occurredTo);
}
private static String intervalKey(EventHubEventDto event) {
JsonNode raw = raw(event);
String intervalId = text(raw, "intervalId");
if (intervalId != null) {
return intervalId;
}
String sourceRowId = text(raw, "sourceRowId");
return sourceRowId != null ? sourceRowId : event.externalSourceEventId();
}
private static JsonNode raw(EventHubEventDto event) {
JsonNode payload = event == null ? null : event.payload();
if (payload == null || payload.isNull() || payload.isMissingNode()) {
return null;
}
JsonNode raw = payload.get("raw");
return raw == null || raw.isNull() ? payload : raw;
}
private static String text(JsonNode node, String field) {
if (node == null || field == null) {
return null;
}
JsonNode value = node.get(field);
if (value == null || value.isNull()) {
return null;
}
String text = value.asText(null);
return text == null || text.isBlank() ? null : text.trim();
}
private static final class IntervalGroup {
private final List<EventHubEventDto> events = new ArrayList<>();
private OffsetDateTime startedAt;
private OffsetDateTime endedAt;
private void add(EventHubEventDto event) {
events.add(event);
if (event == null || event.occurredAt() == null) {
return;
}
if (isStartEvent(event)) {
startedAt = min(startedAt, event.occurredAt());
return;
}
if (isEndEvent(event)) {
endedAt = max(endedAt, event.occurredAt());
return;
}
startedAt = min(startedAt, event.occurredAt());
endedAt = max(endedAt, event.occurredAt());
}
private boolean overlaps(OffsetDateTime occurredFrom, OffsetDateTime occurredTo) {
if (startedAt == null && endedAt == null) {
return false;
}
OffsetDateTime effectiveStart = startedAt == null ? endedAt : startedAt;
OffsetDateTime effectiveEnd = endedAt;
if (occurredTo != null && effectiveStart != null && effectiveStart.isAfter(occurredTo)) {
return false;
}
return occurredFrom == null || effectiveEnd == null || !effectiveEnd.isBefore(occurredFrom);
}
private boolean isStartEvent(EventHubEventDto event) {
return event.lifecycle() == EventLifecycle.START
|| event.eventType() == EventType.CARD_INSERTED;
}
private boolean isEndEvent(EventHubEventDto event) {
return event.lifecycle() == EventLifecycle.END
|| event.eventType() == EventType.CARD_WITHDRAWN;
}
private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) {
if (left == null) {
return right;
}
if (right == null) {
return left;
}
return left.isBefore(right) ? left : right;
}
private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) {
if (left == null) {
return right;
}
if (right == null) {
return left;
}
return left.isAfter(right) ? left : right;
}
}
}

View File

@ -55,7 +55,8 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
sessionId,
request.driverKey(),
request.occurredFrom(),
request.occurredTo()
request.occurredTo(),
request.includeIntersectingIntervals()
)
));
}
@ -77,7 +78,8 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
vehicleRef.registrationNation(),
vehicleRef.registrationNumber(),
request.vehicleOccurredFrom(),
request.vehicleOccurredTo()
request.vehicleOccurredTo(),
request.includeIntersectingIntervals()
)
));
}

View File

@ -5,11 +5,11 @@ import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle;
import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInSessionException;
import at.procon.eventhub.tachographfilesession.service.DriverTimelineEventBuilder;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
import java.time.OffsetDateTime;
import java.util.List;
import org.springframework.stereotype.Component;
@ -38,30 +38,27 @@ public class TachographFileSessionUnifiedDriverEventSource implements UnifiedDri
.orElseThrow(() -> new TachographFileSessionNotFoundException(request.sessionId()));
if (request.driverKey() == null) {
return session.driversByKey().values().stream()
.flatMap(driver -> eventBuilder.buildEvents(session, driver).stream())
.filter(event -> withinWindow(event.occurredAt(), request.occurredFrom(), request.occurredTo()))
.map(driver -> filterBundle(session, driver, request).allEvents())
.flatMap(List::stream)
.toList();
}
DriverExtractionSession driver = session.driversByKey().get(request.driverKey());
if (driver == null) {
throw new DriverNotFoundInSessionException(request.sessionId(), request.driverKey());
}
return eventBuilder.buildEvents(session, driver).stream()
.filter(event -> withinWindow(event.occurredAt(), request.occurredFrom(), request.occurredTo()))
.toList();
return filterBundle(session, driver, request).allEvents();
}
private boolean withinWindow(
OffsetDateTime occurredAt,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
private TachographTimelineEventBundle filterBundle(
TachographFileSession session,
DriverExtractionSession driver,
UnifiedDriverEventsRequest request
) {
if (occurredAt == null) {
return false;
}
if (occurredFrom != null && occurredAt.isBefore(occurredFrom)) {
return false;
}
return occurredTo == null || !occurredAt.isAfter(occurredTo);
return RuntimeIntervalEventWindowSelector.filterBundle(
eventBuilder.buildEventBundle(session, driver),
request.occurredFrom(),
request.occurredTo(),
request.includeIntersectingIntervals()
);
}
}

View File

@ -6,10 +6,10 @@ import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest;
import at.procon.eventhub.reference.TachographNationRegistry;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle;
import at.procon.eventhub.tachographfilesession.service.DriverTimelineEventBuilder;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
import java.time.OffsetDateTime;
import java.util.List;
import org.springframework.stereotype.Component;
@ -37,9 +37,14 @@ public class TachographFileSessionUnifiedVehicleEventSource implements UnifiedVe
TachographFileSession session = repository.find(request.sessionId())
.orElseThrow(() -> new TachographFileSessionNotFoundException(request.sessionId()));
return session.driversByKey().values().stream()
.flatMap(driver -> eventBuilder.buildEvents(session, driver).stream())
.map(driver -> RuntimeIntervalEventWindowSelector.filterBundle(
eventBuilder.buildEventBundle(session, driver),
request.occurredFrom(),
request.occurredTo(),
request.includeIntersectingIntervals()
).allEvents())
.flatMap(List::stream)
.filter(event -> matchesVehicle(event.vehicleRef(), request))
.filter(event -> withinWindow(event.occurredAt(), request.occurredFrom(), request.occurredTo()))
.distinct()
.toList();
}
@ -61,20 +66,6 @@ public class TachographFileSessionUnifiedVehicleEventSource implements UnifiedVe
&& matchesNation(request.registrationNation(), vehicleRef.vehicleRegistration().nation(), vehicleRef.vehicleRegistration().nationNumericCode());
}
private boolean withinWindow(
OffsetDateTime occurredAt,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo
) {
if (occurredAt == null) {
return false;
}
if (occurredFrom != null && occurredAt.isBefore(occurredFrom)) {
return false;
}
return occurredTo == null || !occurredAt.isAfter(occurredTo);
}
private boolean matchesNation(String requestedNation, String actualNation, Integer actualNationNumericCode) {
if (requestedNation == null) {
return true;

View File

@ -68,11 +68,10 @@ public class UnifiedEventTimelineReconstructor {
continue;
}
JsonNode raw = raw(event);
String intervalId = firstNonBlank(
text(raw, "intervalId"),
text(raw, "sourceRowId"),
event.externalSourceEventId()
);
String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), event.externalSourceEventId());
if (intervalId == null) {
continue;
}
ActivityAccumulator accumulator = byIntervalId.computeIfAbsent(
intervalId,
ignored -> new ActivityAccumulator(intervalId)
@ -102,11 +101,10 @@ public class UnifiedEventTimelineReconstructor {
continue;
}
JsonNode raw = raw(event);
String intervalId = firstNonBlank(
text(raw, "intervalId"),
text(raw, "sourceRowId"),
event.externalSourceEventId()
);
String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), event.externalSourceEventId());
if (intervalId == null) {
continue;
}
VehicleUsageAccumulator accumulator = byIntervalId.computeIfAbsent(
intervalId,
ignored -> new VehicleUsageAccumulator(sessionId, driverKey, intervalId)

View File

@ -167,7 +167,11 @@ public class UnifiedRuntimeDerivedProjectionService {
.toList(),
notes
);
DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(processingInput);
DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(processingInput)
.withIncludedIntervals(
apiRequest.includeActivityIntervalsOrDefault(),
apiRequest.includeDrivingIntervalsOrDefault()
);
notes = projection.notes();
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto(

View File

@ -145,4 +145,5 @@ final class TachographRawPayloadSupport {
|| message.toLowerCase(java.util.Locale.ROOT).contains("not valid")
|| message.toLowerCase(java.util.Locale.ROOT).contains("invalid")));
}
}

View File

@ -150,6 +150,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
Map<String, Object> raw = new LinkedHashMap<>();
raw.put("intervalId", interval.intervalId());
raw.put("sourceRowId", interval.intervalId());
raw.put("sessionId", session.sessionId().toString());
raw.put("driverKey", driverKey);
raw.put("activityType", interval.activityType());
raw.put("sourceRowIds", interval.sourceIntervalIds());
@ -232,6 +233,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
Map<String, Object> raw = new LinkedHashMap<>();
raw.put("intervalId", interval.intervalId());
raw.put("sourceRowId", interval.intervalId());
raw.put("sessionId", session.sessionId().toString());
raw.put("driverKey", driverKey);
raw.put("sourceRowIds", interval.sourceIntervalIds());
raw.put("startedAt", timeText(interval.from()));
@ -312,6 +314,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
EventDetailsDto details = supportDetails(eventDomain, supportEvent);
Map<String, Object> raw = new LinkedHashMap<>();
raw.put("sourceRowId", supportEvent.eventId());
raw.put("sessionId", session.sessionId().toString());
raw.put("supportEventId", supportEvent.eventId());
raw.put("driverKey", supportEvent.driverKey());
raw.put("supportEventType", supportEvent.eventType());

View File

@ -28,7 +28,6 @@ create schema DriverActivityIntervalEvent(
);
create window OpenDriverActivityPoint#unique(driverKey, intervalId) as DriverActivityPointEvent;
insert into OpenDriverActivityPoint
select *
from DriverActivityPointEvent(lifecycle = 'START');

View File

@ -0,0 +1,106 @@
package at.procon.eventhub.processing.driverworkingtime.dto;
import static org.assertj.core.api.Assertions.assertThat;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.Test;
class DriverWorkingTimeProcessingResultDtoTest {
@Test
void canExcludeActivityAndDrivingIntervalsWhileKeepingCounts() {
DriverWorkingTimeActivityInterval activityInterval = new DriverWorkingTimeActivityInterval(
null,
"12:123",
"ACT-1",
"WORK",
null,
null,
null,
null,
null,
"DRIVER_CARD",
null,
null,
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T09:00:00Z"),
OffsetDateTime.parse("2026-05-01T08:00:00Z").toEpochSecond(),
OffsetDateTime.parse("2026-05-01T09:00:00Z").toEpochSecond(),
3600L,
List.of("ACT-1"),
false,
false,
"RAW_INTERVAL"
);
DriverWorkingTimeActivityInterval drivingInterval = new DriverWorkingTimeActivityInterval(
null,
"12:123",
"DRV-1",
"DRIVE",
null,
null,
null,
null,
null,
"DRIVER_CARD",
null,
null,
OffsetDateTime.parse("2026-05-01T09:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
OffsetDateTime.parse("2026-05-01T09:00:00Z").toEpochSecond(),
OffsetDateTime.parse("2026-05-01T10:00:00Z").toEpochSecond(),
3600L,
List.of("DRV-1"),
false,
false,
"RAW_INTERVAL"
);
DriverWorkingTimeProcessingResultDto result = new DriverWorkingTimeProcessingResultDto(
UUID.randomUUID(),
"12:123",
"UNIFIED_EVENT_STREAM",
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"),
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
List.of(activityInterval),
List.of(drivingInterval),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of("note")
);
DriverWorkingTimeProcessingResultDto compact = result.withIncludedIntervals(false, false);
assertThat(compact.activityIntervalCount()).isEqualTo(1);
assertThat(compact.drivingIntervalCount()).isEqualTo(1);
assertThat(compact.activityIntervals()).isEmpty();
assertThat(compact.drivingIntervals()).isEmpty();
assertThat(compact.notes()).containsExactly("note");
}
}

View File

@ -49,6 +49,9 @@ class RuntimeEventProcessingServiceTest {
true,
0,
null,
null,
null,
null,
null
),
new RuntimeEventPartitioningApiRequest(RuntimeEventPartitioningStrategy.DRIVER, null, false, null, false, null, false, null, null, null),

View File

@ -0,0 +1,219 @@
package at.procon.eventhub.processing.eventprocessing.module;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDriverPartition;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimePreparedInput;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput;
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
class DriverWorkingTimeDerivedProjectionsModuleTest {
@Test
void appliesIncludeFlagsOnModulePath() {
DriverWorkingTimeProcessingCore core = org.mockito.Mockito.mock(DriverWorkingTimeProcessingCore.class);
DriverWorkingTimeDerivedProjectionsModule module = new DriverWorkingTimeDerivedProjectionsModule(core);
DriverWorkingTimeActivityInterval activityInterval = new DriverWorkingTimeActivityInterval(
null,
"12:123",
"ACT-1",
"WORK",
null,
null,
null,
null,
null,
"DRIVER_CARD",
null,
null,
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T09:00:00Z"),
OffsetDateTime.parse("2026-05-01T08:00:00Z").toEpochSecond(),
OffsetDateTime.parse("2026-05-01T09:00:00Z").toEpochSecond(),
3600L,
List.of("ACT-1"),
false,
false,
"RAW_INTERVAL"
);
DriverWorkingTimeActivityInterval drivingInterval = new DriverWorkingTimeActivityInterval(
null,
"12:123",
"DRV-1",
"DRIVE",
null,
null,
null,
null,
null,
"DRIVER_CARD",
null,
null,
OffsetDateTime.parse("2026-05-01T09:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
OffsetDateTime.parse("2026-05-01T09:00:00Z").toEpochSecond(),
OffsetDateTime.parse("2026-05-01T10:00:00Z").toEpochSecond(),
3600L,
List.of("DRV-1"),
false,
false,
"RAW_INTERVAL"
);
DriverWorkingTimeProcessingResultDto rawProjection = new DriverWorkingTimeProcessingResultDto(
UUID.randomUUID(),
"12:123",
"UNIFIED_EVENT_STREAM",
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"),
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
List.of(activityInterval),
List.of(drivingInterval),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of("note")
);
when(core.process(any())).thenReturn(rawProjection);
UnifiedRuntimeProcessingApiRequest scope = new UnifiedRuntimeProcessingApiRequest(
UUID.randomUUID(),
List.of(),
null,
null,
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
null,
null,
"12:123",
Set.of(),
false,
Set.of(),
false,
null,
null,
null,
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
true,
0,
null,
null,
null,
false,
false
);
UnifiedRuntimeProcessingRequest runtimeRequest = scope.toRuntimeRequest();
DriverWorkingTimeProcessingInput processingInput = new DriverWorkingTimeProcessingInput(
null,
"12:123",
"UNIFIED_EVENT_STREAM",
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"),
3,
720,
List.of(),
List.of(),
List.of(),
List.of()
);
DriverWorkingTimePreparedInput preparedInput = new DriverWorkingTimePreparedInput(
"12:123",
new DriverWorkingTimeDriverPartition(
"12:123",
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
null,
List.of(),
null,
List.of(),
List.of()
),
processingInput
);
UnifiedRuntimeEventBundle bundle = new UnifiedRuntimeEventBundle(
runtimeRequest,
List.of(),
List.of(),
List.of(),
List.of(),
List.of()
);
RuntimeProcessingModuleContext context = new RuntimeProcessingModuleContext(
new RuntimeProcessingExecutionApiRequest("driver-working-time-v1", scope, null, List.of(), Map.of()),
List.of(),
Map.of("runtimeScopeApiRequest", scope),
Map.of(
DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY,
new RuntimeProcessingModuleResult(
DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY,
RuntimeProcessingModuleStatus.SUCCESS,
bundle,
Map.of(),
List.of()
),
DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
new RuntimeProcessingModuleResult(
DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
RuntimeProcessingModuleStatus.SUCCESS,
Map.of("12:123", preparedInput),
Map.of(),
List.of()
)
)
);
RuntimeProcessingModuleResult result = module.execute(context);
UnifiedRuntimeDriverWorkingTimeScopeResultDto scopeResult =
(UnifiedRuntimeDriverWorkingTimeScopeResultDto) result.output();
UnifiedRuntimeDerivedProjectionResultDto driverResult = scopeResult.driverResults().get("12:123");
assertThat(driverResult.projection().activityIntervalCount()).isEqualTo(1);
assertThat(driverResult.projection().drivingIntervalCount()).isEqualTo(1);
assertThat(driverResult.projection().activityIntervals()).isEmpty();
assertThat(driverResult.projection().drivingIntervals()).isEmpty();
}
}

View File

@ -40,6 +40,8 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
"minimumRestPeriodMinutes",
"attachVehicleOnlyEvents",
"vehicleEvidencePaddingMinutes",
"includeActivityIntervals",
"includeDrivingIntervals",
"includePartitionDebug"
);
}
@ -71,6 +73,9 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
true,
15,
null,
null,
null,
null,
null
);
RuntimeEventProcessingApiRequest request = new RuntimeEventProcessingApiRequest(
@ -93,6 +98,8 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
"minimumRestPeriodMinutes", "600",
"vehicleEvidencePaddingMinutes", 20,
"attachVehicleOnlyEvents", true,
"includeActivityIntervals", true,
"includeDrivingIntervals", true,
"includePartitionDebug", true
)
);
@ -116,7 +123,8 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
OffsetDateTime.parse("2026-05-31T23:59:59Z"),
true,
15
15,
true
);
UnifiedRuntimeDerivedProjectionResultDto driverResult = new UnifiedRuntimeDerivedProjectionResultDto(
processedRequest,
@ -158,6 +166,8 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
assertThat(delegated.minimumRestPeriodMinutes()).isEqualTo(600);
assertThat(delegated.vehicleExpansionPaddingMinutes()).isEqualTo(20);
assertThat(delegated.expandVehicleEvents()).isTrue();
assertThat(delegated.includeActivityIntervals()).isTrue();
assertThat(delegated.includeDrivingIntervals()).isTrue();
assertThat(debugCaptor.getValue()).isTrue();
}
}

View File

@ -99,8 +99,11 @@ class RuntimeMixedSourceEvidenceValidationServiceTest {
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
true,
15,
null,
3,
720
720,
null,
null
),
new RuntimeEventPartitioningApiRequest(
RuntimeEventPartitioningStrategy.DRIVER,

View File

@ -82,7 +82,8 @@ class UnifiedDriverEventsRequestTest {
null,
null,
null,
null
null,
false
)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("At least one driver or vehicle selector");
}

View File

@ -34,6 +34,7 @@ class UnifiedRuntimeProcessingRequestTest {
assertThat(request.eventBackend()).isEqualTo(UnifiedRuntimeEventBackend.SOURCE_DB);
assertThat(request.expandVehicleEvents()).isTrue();
assertThat(request.vehicleOccurredFrom()).isEqualTo(OffsetDateTime.parse("2026-05-01T00:00:00Z"));
assertThat(request.includeIntersectingIntervals()).isTrue();
}
@Test
@ -92,7 +93,8 @@ class UnifiedRuntimeProcessingRequestTest {
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
true,
0
0,
true
);
assertThat(driverCardOnlyRequest.tachographSourceKinds()).containsExactly(UnifiedTachographSourceKind.DRIVER_CARD);
@ -195,7 +197,8 @@ class UnifiedRuntimeProcessingRequestTest {
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
true,
10
10,
true
);
assertThat(request.driverKey()).isNull();
@ -224,7 +227,8 @@ class UnifiedRuntimeProcessingRequestTest {
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
true,
10
10,
true
);
assertThat(request.includeAllDrivers()).isTrue();
@ -254,7 +258,8 @@ class UnifiedRuntimeProcessingRequestTest {
null,
null,
true,
0
0,
true
)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Use either compositeSessionId");
}
@ -281,7 +286,8 @@ class UnifiedRuntimeProcessingRequestTest {
null,
null,
true,
0
0,
true
)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("At least one driver selector");
}

View File

@ -63,7 +63,8 @@ class UnifiedVehicleEventsRequestTest {
null,
null,
null,
null
null,
false
)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("At least one vehicle selector");
}

View File

@ -84,6 +84,8 @@ class EventHubRuntimeEventLoaderTest {
assertThat(driverRequest.occurredFrom()).isEqualTo(OffsetDateTime.parse("2026-05-01T00:00:00Z"));
assertThat(driverRequest.occurredTo()).isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z"));
});
assertThat(driverSource.requests).extracting(UnifiedDriverEventsRequest::includeIntersectingIntervals)
.containsExactly(true, false);
}
@Test
@ -121,6 +123,8 @@ class EventHubRuntimeEventLoaderTest {
assertThat(vehicleRequest.occurredFrom()).isEqualTo(OffsetDateTime.parse("2026-04-30T23:45:00Z"));
assertThat(vehicleRequest.occurredTo()).isEqualTo(OffsetDateTime.parse("2026-05-02T00:15:00Z"));
});
assertThat(vehicleSource.requests).extracting(UnifiedVehicleEventsRequest::includeIntersectingIntervals)
.containsExactly(true, false);
}
private static final class CapturingDriverSource implements UnifiedDriverEventSource {

View File

@ -76,6 +76,49 @@ class TachographFileSessionRuntimeEventLoaderTest {
assertThat(loader.loadVehicleEvents(request, new UnifiedDiscoveredVehicleRef("VIN-1", "VIN-1", "12", "REG-1"))).hasSize(5);
}
@Test
void keepsCompleteIntersectingIntervalsWhenRequestStartsInsideInterval() {
EventHubProperties properties = new EventHubProperties();
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
InMemoryTachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository();
IntervalBackedDriverTimelineEventBuilder eventBuilder = new IntervalBackedDriverTimelineEventBuilder(
new DriverTimelineBuilder(),
new DriverKeyFactory(),
new VehicleKeyFactory(),
new EventDetailsFactory(new ObjectMapper())
);
TachographFileSessionRuntimeEventLoader loader = new TachographFileSessionRuntimeEventLoader(
new UnifiedDriverEventSourceService(List.of(new TachographFileSessionUnifiedDriverEventSource(repository, eventBuilder))),
new UnifiedVehicleEventSourceService(List.of(new TachographFileSessionUnifiedVehicleEventSource(repository, eventBuilder))),
compositeRepository,
new EventAcquisitionRecordKeyService(),
new EventHubEventSorter()
);
DriverExtractionSession driver = driver();
TachographFileSession session = session(driver);
repository.save(session);
UnifiedRuntimeProcessingRequest request = UnifiedRuntimeProcessingRequest.forTachographFileSession(
session.sessionId(),
driver.driverKey(),
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
OffsetDateTime.parse("2026-05-01T09:15:00Z"),
true,
0
);
assertThat(loader.loadDriverEvents(request))
.extracting(event -> event.occurredAt())
.containsExactly(
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
OffsetDateTime.parse("2026-05-01T09:00:00Z"),
OffsetDateTime.parse("2026-05-01T10:00:00Z")
);
}
@Test

View File

@ -60,7 +60,8 @@ class UnifiedRuntimeDriverTimelineServiceTest {
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
false,
0
0,
true
)
);

View File

@ -52,7 +52,8 @@ class UnifiedRuntimeEventAssemblyServiceTest {
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
true,
15
15,
true
)
);