From 6bef8becf96ff1b34fa2a17152a629ce53553431 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:19:21 +0200 Subject: [PATCH] Adjust runtime interval loading and result projections --- .../EventHubEventReadRepository.java | 119 +++++++++- .../DriverWorkingTimeProcessingResultDto.java | 44 ++++ .../UnifiedRuntimeProcessingApiRequest.java | 16 +- ...erWorkingTimeDerivedProjectionsModule.java | 6 +- ...riverWorkingTimeRuntimeProcessingPlan.java | 11 +- ...timeTachographParityValidationService.java | 5 +- .../model/UnifiedDriverEventsRequest.java | 71 +++++- .../UnifiedRuntimeProcessingRequest.java | 55 ++++- .../model/UnifiedVehicleEventsRequest.java | 84 ++++++- .../service/EventHubRuntimeEventLoader.java | 6 +- .../RuntimeIntervalEventWindowSelector.java | 180 ++++++++++++++ ...chographFileSessionRuntimeEventLoader.java | 6 +- ...phFileSessionUnifiedDriverEventSource.java | 31 ++- ...hFileSessionUnifiedVehicleEventSource.java | 25 +- .../UnifiedEventTimelineReconstructor.java | 18 +- ...nifiedRuntimeDerivedProjectionService.java | 6 +- .../service/TachographRawPayloadSupport.java | 1 + ...ervalBackedDriverTimelineEventBuilder.java | 3 + .../runtime-driver-activity-intervals.epl | 1 - ...verWorkingTimeProcessingResultDtoTest.java | 106 +++++++++ .../RuntimeEventProcessingServiceTest.java | 3 + ...rkingTimeDerivedProjectionsModuleTest.java | 219 ++++++++++++++++++ ...sperRuntimeEventProcessingProfileTest.java | 12 +- ...edSourceEvidenceValidationServiceTest.java | 5 +- .../model/UnifiedDriverEventsRequestTest.java | 3 +- .../UnifiedRuntimeProcessingRequestTest.java | 16 +- .../UnifiedVehicleEventsRequestTest.java | 3 +- .../EventHubRuntimeEventLoaderTest.java | 4 + ...raphFileSessionRuntimeEventLoaderTest.java | 43 ++++ ...ifiedRuntimeDriverTimelineServiceTest.java | 3 +- ...nifiedRuntimeEventAssemblyServiceTest.java | 3 +- 31 files changed, 1013 insertions(+), 95 deletions(-) create mode 100644 src/main/java/at/procon/eventhub/processing/service/RuntimeIntervalEventWindowSelector.java create mode 100644 src/test/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDtoTest.java create mode 100644 src/test/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModuleTest.java diff --git a/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java b/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java index e7f10d4..268b3fb 100644 --- a/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java +++ b/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java @@ -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 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 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 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); diff --git a/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java b/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java index 01c2ddf..8461066 100644 --- a/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java +++ b/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java @@ -47,4 +47,48 @@ public record DriverWorkingTimeProcessingResultDto( List supportGeoEvents, List 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 + ); + } } diff --git a/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeProcessingApiRequest.java b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeProcessingApiRequest.java index fc6f987..a9c97e1 100644 --- a/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeProcessingApiRequest.java +++ b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeProcessingApiRequest.java @@ -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; + } } diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModule.java index 74f22bf..afd19cd 100644 --- a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModule.java +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModule.java @@ -69,7 +69,11 @@ public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcess for (Map.Entry 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( diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/DriverWorkingTimeRuntimeProcessingPlan.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/DriverWorkingTimeRuntimeProcessingPlan.java index 17fca6b..d7b467a 100644 --- a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/DriverWorkingTimeRuntimeProcessingPlan.java +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/DriverWorkingTimeRuntimeProcessingPlan.java @@ -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 ); } diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationService.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationService.java index a363b50..290ba67 100644 --- a/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationService.java +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationService.java @@ -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, diff --git a/src/main/java/at/procon/eventhub/processing/model/UnifiedDriverEventsRequest.java b/src/main/java/at/procon/eventhub/processing/model/UnifiedDriverEventsRequest.java index b74d7fe..fbeb1db 100644 --- a/src/main/java/at/procon/eventhub/processing/model/UnifiedDriverEventsRequest.java +++ b/src/main/java/at/procon/eventhub/processing/model/UnifiedDriverEventsRequest.java @@ -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 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 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 ); } diff --git a/src/main/java/at/procon/eventhub/processing/model/UnifiedRuntimeProcessingRequest.java b/src/main/java/at/procon/eventhub/processing/model/UnifiedRuntimeProcessingRequest.java index 0712977..259262b 100644 --- a/src/main/java/at/procon/eventhub/processing/model/UnifiedRuntimeProcessingRequest.java +++ b/src/main/java/at/procon/eventhub/processing/model/UnifiedRuntimeProcessingRequest.java @@ -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 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 ); } diff --git a/src/main/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequest.java b/src/main/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequest.java index 0240e85..723d1fa 100644 --- a/src/main/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequest.java +++ b/src/main/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequest.java @@ -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 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 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 ); } diff --git a/src/main/java/at/procon/eventhub/processing/service/EventHubRuntimeEventLoader.java b/src/main/java/at/procon/eventhub/processing/service/EventHubRuntimeEventLoader.java index 91702e1..57649b7 100644 --- a/src/main/java/at/procon/eventhub/processing/service/EventHubRuntimeEventLoader.java +++ b/src/main/java/at/procon/eventhub/processing/service/EventHubRuntimeEventLoader.java @@ -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(), diff --git a/src/main/java/at/procon/eventhub/processing/service/RuntimeIntervalEventWindowSelector.java b/src/main/java/at/procon/eventhub/processing/service/RuntimeIntervalEventWindowSelector.java new file mode 100644 index 0000000..b0b7552 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/service/RuntimeIntervalEventWindowSelector.java @@ -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 filterIntervalEvents( + List events, + OffsetDateTime occurredFrom, + OffsetDateTime occurredTo, + boolean includeIntersectingIntervals + ) { + if (events == null || events.isEmpty()) { + return List.of(); + } + if (!includeIntersectingIntervals) { + return filterPointEvents(events, occurredFrom, occurredTo); + } + + LinkedHashMap byInterval = new LinkedHashMap<>(); + for (EventHubEventDto event : events) { + byInterval.computeIfAbsent(intervalKey(event), ignored -> new IntervalGroup()).add(event); + } + + List result = new ArrayList<>(); + for (IntervalGroup group : byInterval.values()) { + if (group.overlaps(occurredFrom, occurredTo)) { + result.addAll(group.events); + } + } + return List.copyOf(result); + } + + private static List filterPointEvents( + List 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 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; + } + } +} diff --git a/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionRuntimeEventLoader.java b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionRuntimeEventLoader.java index 157946e..70539a1 100644 --- a/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionRuntimeEventLoader.java +++ b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionRuntimeEventLoader.java @@ -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() ) )); } diff --git a/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedDriverEventSource.java b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedDriverEventSource.java index 7a277bb..fc1721f 100644 --- a/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedDriverEventSource.java +++ b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedDriverEventSource.java @@ -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() + ); } } diff --git a/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java index 8b1ee03..139d222 100644 --- a/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java +++ b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java @@ -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; diff --git a/src/main/java/at/procon/eventhub/processing/service/UnifiedEventTimelineReconstructor.java b/src/main/java/at/procon/eventhub/processing/service/UnifiedEventTimelineReconstructor.java index 6e1e1ab..6020019 100644 --- a/src/main/java/at/procon/eventhub/processing/service/UnifiedEventTimelineReconstructor.java +++ b/src/main/java/at/procon/eventhub/processing/service/UnifiedEventTimelineReconstructor.java @@ -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) diff --git a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java index 4aa1729..06b67d8 100644 --- a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java +++ b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java @@ -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( diff --git a/src/main/java/at/procon/eventhub/tachograph/service/TachographRawPayloadSupport.java b/src/main/java/at/procon/eventhub/tachograph/service/TachographRawPayloadSupport.java index 6c928f7..7e0508d 100644 --- a/src/main/java/at/procon/eventhub/tachograph/service/TachographRawPayloadSupport.java +++ b/src/main/java/at/procon/eventhub/tachograph/service/TachographRawPayloadSupport.java @@ -145,4 +145,5 @@ final class TachographRawPayloadSupport { || message.toLowerCase(java.util.Locale.ROOT).contains("not valid") || message.toLowerCase(java.util.Locale.ROOT).contains("invalid"))); } + } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java index b98f5d4..1f61482 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java @@ -150,6 +150,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE Map 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 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 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()); diff --git a/src/main/resources/esper/runtime-driver-activity-intervals.epl b/src/main/resources/esper/runtime-driver-activity-intervals.epl index b41eae8..bed53bc 100644 --- a/src/main/resources/esper/runtime-driver-activity-intervals.epl +++ b/src/main/resources/esper/runtime-driver-activity-intervals.epl @@ -28,7 +28,6 @@ create schema DriverActivityIntervalEvent( ); create window OpenDriverActivityPoint#unique(driverKey, intervalId) as DriverActivityPointEvent; - insert into OpenDriverActivityPoint select * from DriverActivityPointEvent(lifecycle = 'START'); diff --git a/src/test/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDtoTest.java b/src/test/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDtoTest.java new file mode 100644 index 0000000..88f661e --- /dev/null +++ b/src/test/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDtoTest.java @@ -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"); + } +} diff --git a/src/test/java/at/procon/eventhub/processing/eventprocessing/RuntimeEventProcessingServiceTest.java b/src/test/java/at/procon/eventhub/processing/eventprocessing/RuntimeEventProcessingServiceTest.java index a0db82e..85d32e3 100644 --- a/src/test/java/at/procon/eventhub/processing/eventprocessing/RuntimeEventProcessingServiceTest.java +++ b/src/test/java/at/procon/eventhub/processing/eventprocessing/RuntimeEventProcessingServiceTest.java @@ -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), diff --git a/src/test/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModuleTest.java b/src/test/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModuleTest.java new file mode 100644 index 0000000..714efd8 --- /dev/null +++ b/src/test/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModuleTest.java @@ -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(); + } +} diff --git a/src/test/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfileTest.java b/src/test/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfileTest.java index d96f61d..94e78b1 100644 --- a/src/test/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfileTest.java +++ b/src/test/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfileTest.java @@ -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(); } } diff --git a/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationServiceTest.java b/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationServiceTest.java index d30af0d..b955e43 100644 --- a/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationServiceTest.java +++ b/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationServiceTest.java @@ -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, diff --git a/src/test/java/at/procon/eventhub/processing/model/UnifiedDriverEventsRequestTest.java b/src/test/java/at/procon/eventhub/processing/model/UnifiedDriverEventsRequestTest.java index 75a2fd4..7324061 100644 --- a/src/test/java/at/procon/eventhub/processing/model/UnifiedDriverEventsRequestTest.java +++ b/src/test/java/at/procon/eventhub/processing/model/UnifiedDriverEventsRequestTest.java @@ -82,7 +82,8 @@ class UnifiedDriverEventsRequestTest { null, null, null, - null + null, + false )).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("At least one driver or vehicle selector"); } diff --git a/src/test/java/at/procon/eventhub/processing/model/UnifiedRuntimeProcessingRequestTest.java b/src/test/java/at/procon/eventhub/processing/model/UnifiedRuntimeProcessingRequestTest.java index 11eeea9..a78c3c8 100644 --- a/src/test/java/at/procon/eventhub/processing/model/UnifiedRuntimeProcessingRequestTest.java +++ b/src/test/java/at/procon/eventhub/processing/model/UnifiedRuntimeProcessingRequestTest.java @@ -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"); } diff --git a/src/test/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequestTest.java b/src/test/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequestTest.java index 3f2b7f6..5525d41 100644 --- a/src/test/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequestTest.java +++ b/src/test/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequestTest.java @@ -63,7 +63,8 @@ class UnifiedVehicleEventsRequestTest { null, null, null, - null + null, + false )).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("At least one vehicle selector"); } diff --git a/src/test/java/at/procon/eventhub/processing/service/EventHubRuntimeEventLoaderTest.java b/src/test/java/at/procon/eventhub/processing/service/EventHubRuntimeEventLoaderTest.java index 129b6d6..26c977e 100644 --- a/src/test/java/at/procon/eventhub/processing/service/EventHubRuntimeEventLoaderTest.java +++ b/src/test/java/at/procon/eventhub/processing/service/EventHubRuntimeEventLoaderTest.java @@ -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 { diff --git a/src/test/java/at/procon/eventhub/processing/service/TachographFileSessionRuntimeEventLoaderTest.java b/src/test/java/at/procon/eventhub/processing/service/TachographFileSessionRuntimeEventLoaderTest.java index 89294cd..6fa1547 100644 --- a/src/test/java/at/procon/eventhub/processing/service/TachographFileSessionRuntimeEventLoaderTest.java +++ b/src/test/java/at/procon/eventhub/processing/service/TachographFileSessionRuntimeEventLoaderTest.java @@ -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 diff --git a/src/test/java/at/procon/eventhub/processing/service/UnifiedRuntimeDriverTimelineServiceTest.java b/src/test/java/at/procon/eventhub/processing/service/UnifiedRuntimeDriverTimelineServiceTest.java index 683f8fe..68692aa 100644 --- a/src/test/java/at/procon/eventhub/processing/service/UnifiedRuntimeDriverTimelineServiceTest.java +++ b/src/test/java/at/procon/eventhub/processing/service/UnifiedRuntimeDriverTimelineServiceTest.java @@ -60,7 +60,8 @@ class UnifiedRuntimeDriverTimelineServiceTest { OffsetDateTime.parse("2026-05-01T00:00:00Z"), OffsetDateTime.parse("2026-05-02T00:00:00Z"), false, - 0 + 0, + true ) ); diff --git a/src/test/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyServiceTest.java b/src/test/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyServiceTest.java index b12d8ee..bc4175c 100644 --- a/src/test/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyServiceTest.java +++ b/src/test/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyServiceTest.java @@ -52,7 +52,8 @@ class UnifiedRuntimeEventAssemblyServiceTest { OffsetDateTime.parse("2026-05-01T00:00:00Z"), OffsetDateTime.parse("2026-05-02T00:00:00Z"), true, - 15 + 15, + true ) );