From 1128bd3f568b86c47724437accdffc2f4566bd65 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Fri, 22 May 2026 15:40:35 +0200 Subject: [PATCH] Simplify rest card absence metrics --- .../TachographCompositeSessionController.java | 63 ++++ ...eateTachographCompositeSessionRequest.java | 11 + ...ateTachographCompositeSessionResponse.java | 6 + ...chographCompositeDriverEventsResponse.java | 14 + ...phCompositeSessionListDriversResponse.java | 10 + .../TachographCompositeSessionSummaryDto.java | 15 + .../model/TachographCompositeSession.java | 17 + ...klyRestCandidateCoverageIntervalEvent.java | 6 +- ...tentialHomeOvernightStayIntervalEvent.java | 6 +- ...alInVehicleOvernightStayIntervalEvent.java | 6 +- ...erPotentialInVehicleTripIntervalEvent.java | 3 +- ...erNotFoundInCompositeSessionException.java | 10 + .../service/DriverTimelineBuilder.java | 6 +- ...iverTimelineReusableProjectionBuilder.java | 231 ++++++++++--- ...yTachographCompositeSessionRepository.java | 51 +++ ...raphCompositeSessionNotFoundException.java | 10 + .../TachographCompositeSessionRepository.java | 17 + .../TachographCompositeSessionService.java | 261 +++++++++++++++ ...achographFileSessionProcessingService.java | 88 +---- ...raph-driving-derived-projection-bundle.epl | 165 ++++------ ...al-home-overnight-stay-interval-events.epl | 4 +- ...hographCompositeSessionControllerTest.java | 154 +++++++++ .../TachographFileSessionControllerTest.java | 4 - .../service/DriverTimelineBuilderTest.java | 4 +- ...TimelineReusableProjectionBuilderTest.java | 13 +- ...TachographCompositeSessionServiceTest.java | 306 ++++++++++++++++++ ...graphFileSessionProcessingServiceTest.java | 28 +- 27 files changed, 1229 insertions(+), 280 deletions(-) create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/api/TachographCompositeSessionController.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographCompositeSessionRequest.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographCompositeSessionResponse.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeDriverEventsResponse.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeSessionListDriversResponse.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeSessionSummaryDto.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/model/TachographCompositeSession.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/service/DriverNotFoundInCompositeSessionException.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/service/InMemoryTachographCompositeSessionRepository.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionNotFoundException.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionRepository.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionService.java create mode 100644 src/test/java/at/procon/eventhub/tachographfilesession/api/TachographCompositeSessionControllerTest.java create mode 100644 src/test/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionServiceTest.java diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographCompositeSessionController.java b/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographCompositeSessionController.java new file mode 100644 index 0000000..09b2e71 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographCompositeSessionController.java @@ -0,0 +1,63 @@ +package at.procon.eventhub.tachographfilesession.api; + +import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionRequest; +import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeDriverEventsResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeSessionListDriversResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeSessionSummaryDto; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionService; +import jakarta.validation.Valid; +import java.util.UUID; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/eventhub/tachograph-composite-sessions") +public class TachographCompositeSessionController { + + private final TachographCompositeSessionService service; + + public TachographCompositeSessionController(TachographCompositeSessionService service) { + this.service = service; + } + + @PostMapping + public ResponseEntity createCompositeSession( + @Valid @RequestBody CreateTachographCompositeSessionRequest request + ) { + return ResponseEntity.status(HttpStatus.CREATED).body(service.createCompositeSession(request)); + } + + @GetMapping("/{compositeSessionId}") + public ResponseEntity getCompositeSession(@PathVariable UUID compositeSessionId) { + return ResponseEntity.ok(service.getCompositeSession(compositeSessionId)); + } + + @GetMapping("/{compositeSessionId}/drivers") + public ResponseEntity listDrivers(@PathVariable UUID compositeSessionId) { + return ResponseEntity.ok(service.listDrivers(compositeSessionId)); + } + + @GetMapping("/{compositeSessionId}/drivers/{driverKey}/events") + public ResponseEntity getMergedDriverEvents( + @PathVariable UUID compositeSessionId, + @PathVariable String driverKey + ) { + return ResponseEntity.ok(service.getMergedDriverEvents(compositeSessionId, driverKey)); + } + + @GetMapping("/{compositeSessionId}/drivers/{driverKey}/timeline") + public ResponseEntity getMergedDriverTimeline( + @PathVariable UUID compositeSessionId, + @PathVariable String driverKey + ) { + return ResponseEntity.ok(service.getMergedDriverTimeline(compositeSessionId, driverKey)); + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographCompositeSessionRequest.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographCompositeSessionRequest.java new file mode 100644 index 0000000..a37b407 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographCompositeSessionRequest.java @@ -0,0 +1,11 @@ +package at.procon.eventhub.tachographfilesession.dto; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import java.util.UUID; + +public record CreateTachographCompositeSessionRequest( + @NotEmpty List sessionIds, + String label +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographCompositeSessionResponse.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographCompositeSessionResponse.java new file mode 100644 index 0000000..ed943c4 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographCompositeSessionResponse.java @@ -0,0 +1,6 @@ +package at.procon.eventhub.tachographfilesession.dto; + +public record CreateTachographCompositeSessionResponse( + TachographCompositeSessionSummaryDto session +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeDriverEventsResponse.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeDriverEventsResponse.java new file mode 100644 index 0000000..296717c --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeDriverEventsResponse.java @@ -0,0 +1,14 @@ +package at.procon.eventhub.tachographfilesession.dto; + +import at.procon.eventhub.dto.EventHubEventDto; +import java.util.List; +import java.util.UUID; + +public record TachographCompositeDriverEventsResponse( + UUID compositeSessionId, + String driverKey, + List sourceSessionIds, + int eventCount, + List events +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeSessionListDriversResponse.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeSessionListDriversResponse.java new file mode 100644 index 0000000..f4a5239 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeSessionListDriversResponse.java @@ -0,0 +1,10 @@ +package at.procon.eventhub.tachographfilesession.dto; + +import java.util.List; +import java.util.UUID; + +public record TachographCompositeSessionListDriversResponse( + UUID compositeSessionId, + List drivers +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeSessionSummaryDto.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeSessionSummaryDto.java new file mode 100644 index 0000000..ccb609d --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographCompositeSessionSummaryDto.java @@ -0,0 +1,15 @@ +package at.procon.eventhub.tachographfilesession.dto; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public record TachographCompositeSessionSummaryDto( + UUID compositeSessionId, + String tenantKey, + String label, + List memberSessionIds, + List drivers, + Instant createdAt +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographCompositeSession.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographCompositeSession.java new file mode 100644 index 0000000..a100751 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographCompositeSession.java @@ -0,0 +1,17 @@ +package at.procon.eventhub.tachographfilesession.model; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public record TachographCompositeSession( + UUID compositeSessionId, + String tenantKey, + String label, + List memberSessionIds, + Instant createdAt +) { + public TachographCompositeSession { + memberSessionIds = memberSessionIds == null ? List.of() : List.copyOf(memberSessionIds); + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent.java index 0ee019e..b4e50ed 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent.java @@ -9,10 +9,8 @@ public record TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( OffsetDateTime startedAt, OffsetDateTime endedAt, long durationSeconds, - long cardPresentDurationSeconds, - double cardPresentCoveragePercent, - long unknownDurationSeconds, - double unknownCoveragePercent, + long cardAbsentDurationSeconds, + double cardAbsentCoveragePercent, String previousDrivingSourceIntervalId, String nextDrivingSourceIntervalId, String previousRegistrationKey, diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java index 6ae3794..6496c47 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java @@ -9,10 +9,8 @@ public record TachographEsperPotentialHomeOvernightStayIntervalEvent( OffsetDateTime startedAt, OffsetDateTime endedAt, long durationSeconds, - long cardPresentDurationSeconds, - double cardPresentCoveragePercent, - long unknownDurationSeconds, - double unknownCoveragePercent, + long cardAbsentDurationSeconds, + double cardAbsentCoveragePercent, String previousDrivingSourceIntervalId, String nextDrivingSourceIntervalId, String previousRegistrationKey, diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java index 25cff2a..09ccde9 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java @@ -9,10 +9,8 @@ public record TachographEsperPotentialInVehicleOvernightStayIntervalEvent( OffsetDateTime startedAt, OffsetDateTime endedAt, long durationSeconds, - long cardPresentDurationSeconds, - double cardPresentCoveragePercent, - long unknownDurationSeconds, - double unknownCoveragePercent, + long cardAbsentDurationSeconds, + double cardAbsentCoveragePercent, String previousDrivingSourceIntervalId, String nextDrivingSourceIntervalId, String previousRegistrationKey, diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleTripIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleTripIntervalEvent.java index 122a4c2..e153226 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleTripIntervalEvent.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleTripIntervalEvent.java @@ -14,8 +14,7 @@ public record TachographEsperPotentialInVehicleTripIntervalEvent( String vehicleKey, int containedPotentialInVehicleOvernightStayIntervalCount, long containedPotentialInVehicleOvernightStayDurationSeconds, - long containedCardPresentDurationSeconds, - long containedUnknownDurationSeconds, + long containedCardAbsentDurationSeconds, OffsetDateTime firstPotentialInVehicleOvernightStayStartedAt, OffsetDateTime lastPotentialInVehicleOvernightStayEndedAt, String firstPreviousDrivingSourceIntervalId, diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverNotFoundInCompositeSessionException.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverNotFoundInCompositeSessionException.java new file mode 100644 index 0000000..965664b --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverNotFoundInCompositeSessionException.java @@ -0,0 +1,10 @@ +package at.procon.eventhub.tachographfilesession.service; + +import java.util.UUID; + +public class DriverNotFoundInCompositeSessionException extends RuntimeException { + + public DriverNotFoundInCompositeSessionException(UUID compositeSessionId, String driverKey) { + super("Driver '%s' was not found in tachograph composite session '%s'.".formatted(driverKey, compositeSessionId)); + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java index 5ea6e12..fac9734 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java @@ -997,10 +997,8 @@ public class DriverTimelineBuilder { (OffsetDateTime) event.get("startedAt"), (OffsetDateTime) event.get("endedAt"), (Long) event.get("durationSeconds"), - 0L, - 0.0d, - (Long) event.get("unknownDurationSeconds"), - (Double) event.get("unknownCoveragePercent"), + (Long) event.get("cardAbsentDurationSeconds"), + (Double) event.get("cardAbsentCoveragePercent"), (String) event.get("previousDrivingSourceIntervalId"), (String) event.get("nextDrivingSourceIntervalId"), (String) event.get("previousRegistrationKey"), diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java index 27847d3..6af166e 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java @@ -35,6 +35,7 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; @@ -76,6 +77,54 @@ public class DriverTimelineReusableProjectionBuilder { ); } + public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( + TachographFileSession session, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + if (session == null || session.driversByKey() == null || session.driversByKey().isEmpty()) { + return emptyBundle(); + } + + List> activityInputEvents = new ArrayList<>(); + List> vehicleUsageInputEvents = new ArrayList<>(); + List> supportGeoInputEvents = new ArrayList<>(); + + for (DriverExtractionSession driverSession : session.driversByKey().values()) { + if (driverSession == null || driverSession.driverKey() == null) { + continue; + } + ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driverSession); + if (timeline == null) { + continue; + } + for (ResolvedActivityInterval interval : safeList(timeline.activityIntervals())) { + activityInputEvents.add(toActivityIntervalInputMap( + session.sessionId(), + driverSession.driverKey(), + interval + )); + } + for (ResolvedVehicleUsageInterval interval : safeList(timeline.vehicleUsageIntervals())) { + vehicleUsageInputEvents.add(toVehicleUsageIntervalInputMap(interval)); + } + for (ExtractedSupportEvent supportEvent : safeList(timeline.supportEvents())) { + Map supportGeoEvidence = toSupportGeoEvidenceInputMap(session.sessionId(), supportEvent); + if (supportGeoEvidence != null) { + supportGeoInputEvents.add(supportGeoEvidence); + } + } + } + + return buildEsperDrivingDerivedProjectionBundle( + activityInputEvents, + vehicleUsageInputEvents, + supportGeoInputEvents, + significantDrivingMinutes, + minimumRestPeriodMinutes + ); + } + public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( UUID sessionId, String driverKey, @@ -87,27 +136,23 @@ public class DriverTimelineReusableProjectionBuilder { return emptyBundle(); } return buildEsperDrivingDerivedProjectionBundle( - sessionId, - driverKey, - timeline.activityIntervals(), - timeline.vehicleUsageIntervals(), - timeline.supportEvents(), + buildActivityIntervalInputEvents(sessionId, driverKey, timeline.activityIntervals()), + buildVehicleUsageIntervalInputEvents(timeline.vehicleUsageIntervals()), + buildSupportGeoInputEvents(sessionId, timeline.supportEvents()), significantDrivingMinutes, minimumRestPeriodMinutes ); } private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( - UUID sessionId, - String driverKey, - List activityIntervals, - List vehicleUsageIntervals, - List supportEvents, + List> activityInputEvents, + List> vehicleUsageInputEvents, + List> supportGeoInputEvents, int significantDrivingMinutes, int minimumRestPeriodMinutes ) { - if ((activityIntervals == null || activityIntervals.isEmpty()) - && (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty())) { + if ((activityInputEvents == null || activityInputEvents.isEmpty()) + && (vehicleUsageInputEvents == null || vehicleUsageInputEvents.isEmpty())) { return emptyBundle(); } @@ -149,29 +194,26 @@ public class DriverTimelineReusableProjectionBuilder { "potentialInVehicleTripIntervals", newData -> collectPotentialInVehicleTripIntervalEvents(newData, potentialInVehicleTripIntervals) ), runtime -> { - if (supportEvents != null) { - for (ExtractedSupportEvent supportEvent : supportEvents) { - Map supportGeoEvidence = toSupportGeoEvidenceInputMap(sessionId, supportEvent); - if (supportGeoEvidence != null) { - runtime.getEventService().sendEventMap( - supportGeoEvidence, - "TachographSupportGeoEvidenceInputEvent" - ); - } + if (supportGeoInputEvents != null) { + for (Map supportGeoEvidence : supportGeoInputEvents) { + runtime.getEventService().sendEventMap( + supportGeoEvidence, + "TachographSupportGeoEvidenceInputEvent" + ); } } - if (vehicleUsageIntervals != null) { - for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) { + if (vehicleUsageInputEvents != null) { + for (Map interval : vehicleUsageInputEvents) { runtime.getEventService().sendEventMap( - toVehicleUsageIntervalInputMap(interval), + interval, "TachographVehicleUsageIntervalInputEvent" ); } } - if (activityIntervals != null) { - for (ResolvedActivityInterval interval : activityIntervals) { + if (activityInputEvents != null) { + for (Map interval : activityInputEvents) { runtime.getEventService().sendEventMap( - toActivityIntervalInputMap(sessionId, driverKey, interval), + interval, "TachographActivityIntervalInputEvent" ); } @@ -192,6 +234,50 @@ public class DriverTimelineReusableProjectionBuilder { ); } + private List> buildActivityIntervalInputEvents( + UUID sessionId, + String driverKey, + List activityIntervals + ) { + return safeList(activityIntervals).stream() + .map(interval -> toActivityIntervalInputMap(sessionId, driverKey, interval)) + .sorted(Comparator + .comparing((Map event) -> (Long) event.get("startedAtEpochSecond")) + .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) + .thenComparing(event -> Objects.toString(event.get("intervalId"), ""))) + .toList(); + } + + private List> buildVehicleUsageIntervalInputEvents( + List vehicleUsageIntervals + ) { + return safeList(vehicleUsageIntervals).stream() + .map(this::toVehicleUsageIntervalInputMap) + .sorted(Comparator + .comparing((Map event) -> (Long) event.get("startedAtEpochSecond")) + .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) + .thenComparing(event -> Objects.toString(event.get("intervalId"), ""))) + .toList(); + } + + private List> buildSupportGeoInputEvents( + UUID sessionId, + List supportEvents + ) { + return safeList(supportEvents).stream() + .map(event -> toSupportGeoEvidenceInputMap(sessionId, event)) + .filter(Objects::nonNull) + .sorted(Comparator + .comparing((Map event) -> (Long) event.get("occurredAtEpochSecond")) + .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) + .thenComparing(event -> Objects.toString(event.get("eventId"), ""))) + .toList(); + } + + private List safeList(List values) { + return values == null ? List.of() : values; + } + private TachographEsperDrivingDerivedProjectionBundle emptyBundle() { return new TachographEsperDrivingDerivedProjectionBundle( List.of(), @@ -503,10 +589,8 @@ public class DriverTimelineReusableProjectionBuilder { OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), (Long) event.get("durationSeconds"), - (Long) event.get("cardPresentDurationSeconds"), - (Double) event.get("cardPresentCoveragePercent"), - (Long) event.get("unknownDurationSeconds"), - (Double) event.get("unknownCoveragePercent"), + (Long) event.get("cardAbsentDurationSeconds"), + (Double) event.get("cardAbsentCoveragePercent"), (String) event.get("previousDrivingSourceIntervalId"), (String) event.get("nextDrivingSourceIntervalId"), (String) event.get("previousRegistrationKey"), @@ -571,10 +655,8 @@ public class DriverTimelineReusableProjectionBuilder { OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), (Long) event.get("durationSeconds"), - (Long) event.get("cardPresentDurationSeconds"), - (Double) event.get("cardPresentCoveragePercent"), - (Long) event.get("unknownDurationSeconds"), - (Double) event.get("unknownCoveragePercent"), + (Long) event.get("cardAbsentDurationSeconds"), + (Double) event.get("cardAbsentCoveragePercent"), (String) event.get("previousDrivingSourceIntervalId"), (String) event.get("nextDrivingSourceIntervalId"), (String) event.get("previousRegistrationKey"), @@ -639,10 +721,8 @@ public class DriverTimelineReusableProjectionBuilder { OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), (Long) event.get("durationSeconds"), - (Long) event.get("cardPresentDurationSeconds"), - (Double) event.get("cardPresentCoveragePercent"), - (Long) event.get("unknownDurationSeconds"), - (Double) event.get("unknownCoveragePercent"), + (Long) event.get("cardAbsentDurationSeconds"), + (Double) event.get("cardAbsentCoveragePercent"), (String) event.get("previousDrivingSourceIntervalId"), (String) event.get("nextDrivingSourceIntervalId"), (String) event.get("previousRegistrationKey"), @@ -693,8 +773,7 @@ public class DriverTimelineReusableProjectionBuilder { (String) event.get("vehicleKey"), (Integer) event.get("containedPotentialInVehicleOvernightStayIntervalCount"), (Long) event.get("containedPotentialInVehicleOvernightStayDurationSeconds"), - (Long) event.get("containedCardPresentDurationSeconds"), - (Long) event.get("containedUnknownDurationSeconds"), + (Long) event.get("containedCardAbsentDurationSeconds"), OffsetDateTime.ofInstant(Instant.ofEpochSecond((Long) event.get("firstPotentialInVehicleOvernightStayStartedAtEpochSecond")), ZoneOffset.UTC), OffsetDateTime.ofInstant(Instant.ofEpochSecond((Long) event.get("lastPotentialInVehicleOvernightStayEndedAtEpochSecond")), ZoneOffset.UTC), (String) event.get("firstPreviousDrivingSourceIntervalId"), @@ -725,7 +804,7 @@ public class DriverTimelineReusableProjectionBuilder { private List sortDailyWeeklyRestCandidateCoverageIntervals( List intervals ) { - return intervals.stream() + return deduplicateRestCoverageIntervals(intervals).stream() .sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt) .thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt)) .toList(); @@ -734,7 +813,7 @@ public class DriverTimelineReusableProjectionBuilder { private List sortPotentialHomeOvernightStayIntervals( List intervals ) { - return intervals.stream() + return deduplicatePotentialHomeOvernightStayIntervals(intervals).stream() .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt) .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt)) .toList(); @@ -743,12 +822,76 @@ public class DriverTimelineReusableProjectionBuilder { private List sortPotentialInVehicleOvernightStayIntervals( List intervals ) { - return intervals.stream() + return deduplicatePotentialInVehicleOvernightStayIntervals(intervals).stream() .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt) .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt)) .toList(); } + private List deduplicateRestCoverageIntervals( + List intervals + ) { + Map deduplicated = new LinkedHashMap<>(); + for (TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent interval : intervals) { + deduplicated.put(restCoverageIntervalKey( + interval.driverKey(), + interval.startedAt(), + interval.endedAt(), + interval.previousDrivingSourceIntervalId(), + interval.nextDrivingSourceIntervalId() + ), interval); + } + return new ArrayList<>(deduplicated.values()); + } + + private List deduplicatePotentialHomeOvernightStayIntervals( + List intervals + ) { + Map deduplicated = new LinkedHashMap<>(); + for (TachographEsperPotentialHomeOvernightStayIntervalEvent interval : intervals) { + deduplicated.put(restCoverageIntervalKey( + interval.driverKey(), + interval.startedAt(), + interval.endedAt(), + interval.previousDrivingSourceIntervalId(), + interval.nextDrivingSourceIntervalId() + ), interval); + } + return new ArrayList<>(deduplicated.values()); + } + + private List deduplicatePotentialInVehicleOvernightStayIntervals( + List intervals + ) { + Map deduplicated = new LinkedHashMap<>(); + for (TachographEsperPotentialInVehicleOvernightStayIntervalEvent interval : intervals) { + deduplicated.put(restCoverageIntervalKey( + interval.driverKey(), + interval.startedAt(), + interval.endedAt(), + interval.previousDrivingSourceIntervalId(), + interval.nextDrivingSourceIntervalId() + ), interval); + } + return new ArrayList<>(deduplicated.values()); + } + + private String restCoverageIntervalKey( + String driverKey, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + String previousDrivingSourceIntervalId, + String nextDrivingSourceIntervalId + ) { + return String.join("|", + driverKey == null ? "" : driverKey, + startedAt == null ? "" : startedAt.toString(), + endedAt == null ? "" : endedAt.toString(), + previousDrivingSourceIntervalId == null ? "" : previousDrivingSourceIntervalId, + nextDrivingSourceIntervalId == null ? "" : nextDrivingSourceIntervalId + ); + } + private List sortPotentialInVehicleTripIntervals( List intervals ) { diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/InMemoryTachographCompositeSessionRepository.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/InMemoryTachographCompositeSessionRepository.java new file mode 100644 index 0000000..79bd63a --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/InMemoryTachographCompositeSessionRepository.java @@ -0,0 +1,51 @@ +package at.procon.eventhub.tachographfilesession.service; + +import at.procon.eventhub.tachographfilesession.model.TachographCompositeSession; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.springframework.stereotype.Repository; + +@Repository +public class InMemoryTachographCompositeSessionRepository implements TachographCompositeSessionRepository { + + private final Map sessionsById = new LinkedHashMap<>(); + + @Override + public synchronized TachographCompositeSession save(TachographCompositeSession session) { + TachographCompositeSession toSave = session.createdAt() == null + ? new TachographCompositeSession( + session.compositeSessionId(), + session.tenantKey(), + session.label(), + session.memberSessionIds(), + Instant.now() + ) + : session; + sessionsById.put(toSave.compositeSessionId(), toSave); + return toSave; + } + + @Override + public synchronized Optional find(UUID compositeSessionId) { + return Optional.ofNullable(sessionsById.get(compositeSessionId)); + } + + @Override + public synchronized boolean delete(UUID compositeSessionId) { + return sessionsById.remove(compositeSessionId) != null; + } + + @Override + public synchronized List list() { + List result = new ArrayList<>(sessionsById.values()); + result.sort(Comparator.comparing(TachographCompositeSession::createdAt, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(TachographCompositeSession::compositeSessionId)); + return List.copyOf(result); + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionNotFoundException.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionNotFoundException.java new file mode 100644 index 0000000..107d68b --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionNotFoundException.java @@ -0,0 +1,10 @@ +package at.procon.eventhub.tachographfilesession.service; + +import java.util.UUID; + +public class TachographCompositeSessionNotFoundException extends RuntimeException { + + public TachographCompositeSessionNotFoundException(UUID compositeSessionId) { + super("Tachograph composite session '%s' was not found.".formatted(compositeSessionId)); + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionRepository.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionRepository.java new file mode 100644 index 0000000..d82439c --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionRepository.java @@ -0,0 +1,17 @@ +package at.procon.eventhub.tachographfilesession.service; + +import at.procon.eventhub.tachographfilesession.model.TachographCompositeSession; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface TachographCompositeSessionRepository { + + TachographCompositeSession save(TachographCompositeSession session); + + Optional find(UUID compositeSessionId); + + boolean delete(UUID compositeSessionId); + + List list(); +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionService.java new file mode 100644 index 0000000..09fe45c --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionService.java @@ -0,0 +1,261 @@ +package at.procon.eventhub.tachographfilesession.service; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.processing.service.UnifiedEventTimelineReconstructor; +import at.procon.eventhub.service.EventAcquisitionRecordKeyService; +import at.procon.eventhub.service.EventHubEventSorter; +import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionRequest; +import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeDriverEventsResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeSessionListDriversResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeSessionSummaryDto; +import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverSummaryDto; +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriver; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard; +import at.procon.eventhub.tachographfilesession.model.ExtractionWarning; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.TachographCompositeSession; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import org.springframework.stereotype.Service; + +@Service +public class TachographCompositeSessionService { + + private final TachographCompositeSessionRepository compositeRepository; + private final TachographFileSessionRepository fileSessionRepository; + private final DriverTimelineEventBuilder driverTimelineEventBuilder; + private final UnifiedEventTimelineReconstructor timelineReconstructor; + private final EventAcquisitionRecordKeyService eventKeyService; + private final EventHubEventSorter eventSorter; + + public TachographCompositeSessionService( + TachographCompositeSessionRepository compositeRepository, + TachographFileSessionRepository fileSessionRepository, + DriverTimelineEventBuilder driverTimelineEventBuilder, + UnifiedEventTimelineReconstructor timelineReconstructor, + EventAcquisitionRecordKeyService eventKeyService, + EventHubEventSorter eventSorter + ) { + this.compositeRepository = compositeRepository; + this.fileSessionRepository = fileSessionRepository; + this.driverTimelineEventBuilder = driverTimelineEventBuilder; + this.timelineReconstructor = timelineReconstructor; + this.eventKeyService = eventKeyService; + this.eventSorter = eventSorter; + } + + public CreateTachographCompositeSessionResponse createCompositeSession(CreateTachographCompositeSessionRequest request) { + if (request == null || request.sessionIds() == null || request.sessionIds().isEmpty()) { + throw new IllegalArgumentException("sessionIds must not be empty."); + } + List memberSessionIds = request.sessionIds().stream() + .filter(Objects::nonNull) + .distinct() + .toList(); + if (memberSessionIds.isEmpty()) { + throw new IllegalArgumentException("sessionIds must not be empty."); + } + List sessions = memberSessions(memberSessionIds); + String tenantKey = sessions.stream() + .map(session -> session.metadata() == null ? null : session.metadata().tenantKey()) + .filter(value -> value != null && !value.isBlank()) + .findFirst() + .orElse("default"); + TachographCompositeSession compositeSession = compositeRepository.save(new TachographCompositeSession( + UUID.randomUUID(), + tenantKey, + blankToNull(request.label()), + memberSessionIds, + Instant.now() + )); + return new CreateTachographCompositeSessionResponse(toSummary(compositeSession, sessions)); + } + + public TachographCompositeSessionSummaryDto getCompositeSession(UUID compositeSessionId) { + TachographCompositeSession compositeSession = requireCompositeSession(compositeSessionId); + return toSummary(compositeSession, memberSessions(compositeSession.memberSessionIds())); + } + + public TachographCompositeSessionListDriversResponse listDrivers(UUID compositeSessionId) { + TachographCompositeSession compositeSession = requireCompositeSession(compositeSessionId); + return new TachographCompositeSessionListDriversResponse( + compositeSessionId, + aggregateDriverSummaries(memberSessions(compositeSession.memberSessionIds())) + ); + } + + public TachographCompositeDriverEventsResponse getMergedDriverEvents(UUID compositeSessionId, String driverKey) { + TachographCompositeSession compositeSession = requireCompositeSession(compositeSessionId); + List memberSessions = memberSessions(compositeSession.memberSessionIds()); + List sourceSessions = sessionsWithDriver(memberSessions, driverKey); + if (sourceSessions.isEmpty()) { + throw new DriverNotFoundInCompositeSessionException(compositeSessionId, driverKey); + } + List mergedEvents = mergeDriverEvents(sourceSessions, driverKey); + return new TachographCompositeDriverEventsResponse( + compositeSessionId, + driverKey, + sourceSessions.stream().map(TachographFileSession::sessionId).toList(), + mergedEvents.size(), + mergedEvents + ); + } + + public ResolvedDriverTimeline getMergedDriverTimeline(UUID compositeSessionId, String driverKey) { + TachographCompositeSession compositeSession = requireCompositeSession(compositeSessionId); + List memberSessions = memberSessions(compositeSession.memberSessionIds()); + List sourceSessions = sessionsWithDriver(memberSessions, driverKey); + if (sourceSessions.isEmpty()) { + throw new DriverNotFoundInCompositeSessionException(compositeSessionId, driverKey); + } + List mergedEvents = mergeDriverEvents(sourceSessions, driverKey); + return timelineReconstructor.reconstruct( + compositeSessionId, + driverKey, + mergedEvents, + mergeWarnings(sourceSessions, driverKey), + "COMPOSITE_TACHOGRAPH_FILE_SESSION" + ); + } + + private TachographCompositeSession requireCompositeSession(UUID compositeSessionId) { + return compositeRepository.find(compositeSessionId) + .orElseThrow(() -> new TachographCompositeSessionNotFoundException(compositeSessionId)); + } + + private List memberSessions(List memberSessionIds) { + List sessions = new ArrayList<>(); + for (UUID sessionId : memberSessionIds) { + sessions.add(fileSessionRepository.find(sessionId) + .orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId))); + } + return List.copyOf(sessions); + } + + private List sessionsWithDriver(List sessions, String driverKey) { + return sessions.stream() + .filter(session -> session.driversByKey() != null && session.driversByKey().containsKey(driverKey)) + .toList(); + } + + private List mergeDriverEvents(List sessions, String driverKey) { + LinkedHashMap bySignature = new LinkedHashMap<>(); + for (TachographFileSession session : sessions) { + DriverExtractionSession driverSession = session.driversByKey().get(driverKey); + if (driverSession == null) { + continue; + } + for (EventHubEventDto event : driverTimelineEventBuilder.buildEvents(session, driverSession)) { + bySignature.putIfAbsent(eventKeyService.buildEventSignatureHash(event), event); + } + } + return eventSorter.sort(new ArrayList<>(bySignature.values())); + } + + private List mergeWarnings(List sessions, String driverKey) { + LinkedHashSet warnings = new LinkedHashSet<>(); + for (TachographFileSession session : sessions) { + if (session.warnings() != null) { + warnings.addAll(session.warnings()); + } + DriverExtractionSession driverSession = session.driversByKey().get(driverKey); + if (driverSession != null && driverSession.warnings() != null) { + warnings.addAll(driverSession.warnings()); + } + } + return List.copyOf(warnings); + } + + private TachographCompositeSessionSummaryDto toSummary( + TachographCompositeSession compositeSession, + List memberSessions + ) { + return new TachographCompositeSessionSummaryDto( + compositeSession.compositeSessionId(), + compositeSession.tenantKey(), + compositeSession.label(), + compositeSession.memberSessionIds(), + aggregateDriverSummaries(memberSessions), + compositeSession.createdAt() + ); + } + + private List aggregateDriverSummaries(List sessions) { + Map byDriverKey = new LinkedHashMap<>(); + for (TachographFileSession session : sessions) { + if (session.driversByKey() == null) { + continue; + } + for (DriverExtractionSession driverSession : session.driversByKey().values()) { + if (driverSession == null || driverSession.driverKey() == null) { + continue; + } + byDriverKey.computeIfAbsent(driverSession.driverKey(), ignored -> new DriverSummaryAccumulator(driverSession.driverKey())) + .accept(driverSession); + } + } + return byDriverKey.values().stream() + .map(DriverSummaryAccumulator::finish) + .sorted(Comparator.comparing(TachographFileDriverSummaryDto::driverKey)) + .toList(); + } + + private String blankToNull(String value) { + return value == null || value.isBlank() ? null : value.trim(); + } + + private static final class DriverSummaryAccumulator { + private final String driverKey; + private String surname; + private String firstNames; + private String cardNation; + private String cardNumber; + private int activityIntervalCount; + private int cardVehicleUsageIntervalCount; + + private DriverSummaryAccumulator(String driverKey) { + this.driverKey = driverKey; + } + + private void accept(DriverExtractionSession session) { + ExtractedDriver driver = session.driver(); + ExtractedDriverCard card = session.driverCard(); + if (surname == null && driver != null) { + surname = driver.surname(); + } + if (firstNames == null && driver != null) { + firstNames = driver.firstNames(); + } + if (cardNation == null && card != null) { + cardNation = card.cardNation(); + } + if (cardNumber == null && card != null) { + cardNumber = card.cardNumber(); + } + activityIntervalCount += session.cardActivityIntervals() == null ? 0 : session.cardActivityIntervals().size(); + cardVehicleUsageIntervalCount += session.cardVehicleUsageIntervals() == null ? 0 : session.cardVehicleUsageIntervals().size(); + } + + private TachographFileDriverSummaryDto finish() { + return new TachographFileDriverSummaryDto( + driverKey, + surname, + firstNames, + cardNation, + cardNumber, + activityIntervalCount, + cardVehicleUsageIntervalCount + ); + } + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java index 0ed097a..fd5b0cc 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java @@ -526,25 +526,6 @@ public class TachographFileSessionProcessingService { return null; } long durationSeconds = Duration.between(start, end).getSeconds(); - long cardPresentDurationSeconds = overlapVehicleUsageSeconds( - start, - end, - rawVehicleUsageIntervals, - interval.driverKey(), - null - ); - double cardPresentCoveragePercent = durationSeconds == 0L - ? 0.0d - : (cardPresentDurationSeconds * 100.0d) / durationSeconds; - long unknownDurationSeconds = overlapSeconds( - start, - end, - rawVuCardAbsentIntervals, - interval.driverKey() - ); - double unknownCoveragePercent = durationSeconds == 0L - ? 0.0d - : (unknownDurationSeconds * 100.0d) / durationSeconds; boolean beginBoundaryChanged = !start.equals(interval.startedAt()); boolean endBoundaryChanged = !end.equals(interval.endedAt()); return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( @@ -553,16 +534,16 @@ public class TachographFileSessionProcessingService { start, end, durationSeconds, - cardPresentDurationSeconds, - cardPresentCoveragePercent, - unknownDurationSeconds, - unknownCoveragePercent, + interval.cardAbsentDurationSeconds(), + interval.cardAbsentCoveragePercent(), interval.previousDrivingSourceIntervalId(), interval.nextDrivingSourceIntervalId(), interval.previousRegistrationKey(), interval.nextRegistrationKey(), interval.previousVehicleKey(), interval.nextVehicleKey(), + beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(), + endBoundaryChanged ? null : interval.endBoundaryOdometerKm(), beginBoundaryChanged ? null : interval.beginGeoEventId(), beginBoundaryChanged ? null : interval.beginGeoEventDomain(), beginBoundaryChanged ? null : interval.beginGeoOccurredAt(), @@ -607,25 +588,6 @@ public class TachographFileSessionProcessingService { return null; } long durationSeconds = Duration.between(start, end).getSeconds(); - long cardPresentDurationSeconds = overlapVehicleUsageSeconds( - start, - end, - rawVehicleUsageIntervals, - interval.driverKey(), - null - ); - double cardPresentCoveragePercent = durationSeconds == 0L - ? 0.0d - : (cardPresentDurationSeconds * 100.0d) / durationSeconds; - long unknownDurationSeconds = overlapSeconds( - start, - end, - rawVuCardAbsentIntervals, - interval.driverKey() - ); - double unknownCoveragePercent = durationSeconds == 0L - ? 0.0d - : (unknownDurationSeconds * 100.0d) / durationSeconds; boolean beginBoundaryChanged = !start.equals(interval.startedAt()); boolean endBoundaryChanged = !end.equals(interval.endedAt()); return new TachographEsperPotentialHomeOvernightStayIntervalEvent( @@ -634,16 +596,16 @@ public class TachographFileSessionProcessingService { start, end, durationSeconds, - cardPresentDurationSeconds, - cardPresentCoveragePercent, - unknownDurationSeconds, - unknownCoveragePercent, + interval.cardAbsentDurationSeconds(), + interval.cardAbsentCoveragePercent(), interval.previousDrivingSourceIntervalId(), interval.nextDrivingSourceIntervalId(), interval.previousRegistrationKey(), interval.nextRegistrationKey(), interval.previousVehicleKey(), interval.nextVehicleKey(), + beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(), + endBoundaryChanged ? null : interval.endBoundaryOdometerKm(), beginBoundaryChanged ? null : interval.beginGeoEventId(), beginBoundaryChanged ? null : interval.beginGeoEventDomain(), beginBoundaryChanged ? null : interval.beginGeoOccurredAt(), @@ -688,25 +650,6 @@ public class TachographFileSessionProcessingService { return null; } long durationSeconds = Duration.between(start, end).getSeconds(); - long cardPresentDurationSeconds = overlapVehicleUsageSeconds( - start, - end, - rawVehicleUsageIntervals, - interval.driverKey(), - null - ); - double cardPresentCoveragePercent = durationSeconds == 0L - ? 0.0d - : (cardPresentDurationSeconds * 100.0d) / durationSeconds; - long unknownDurationSeconds = overlapSeconds( - start, - end, - rawVuCardAbsentIntervals, - interval.driverKey() - ); - double unknownCoveragePercent = durationSeconds == 0L - ? 0.0d - : (unknownDurationSeconds * 100.0d) / durationSeconds; boolean beginBoundaryChanged = !start.equals(interval.startedAt()); boolean endBoundaryChanged = !end.equals(interval.endedAt()); return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( @@ -715,16 +658,16 @@ public class TachographFileSessionProcessingService { start, end, durationSeconds, - cardPresentDurationSeconds, - cardPresentCoveragePercent, - unknownDurationSeconds, - unknownCoveragePercent, + interval.cardAbsentDurationSeconds(), + interval.cardAbsentCoveragePercent(), interval.previousDrivingSourceIntervalId(), interval.nextDrivingSourceIntervalId(), interval.previousRegistrationKey(), interval.nextRegistrationKey(), interval.previousVehicleKey(), interval.nextVehicleKey(), + beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(), + endBoundaryChanged ? null : interval.endBoundaryOdometerKm(), beginBoundaryChanged ? null : interval.beginGeoEventId(), beginBoundaryChanged ? null : interval.beginGeoEventDomain(), beginBoundaryChanged ? null : interval.beginGeoOccurredAt(), @@ -803,10 +746,7 @@ public class TachographFileSessionProcessingService { .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::durationSeconds) .sum(), containedIntervals.stream() - .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::cardPresentDurationSeconds) - .sum(), - containedIntervals.stream() - .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::unknownDurationSeconds) + .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::cardAbsentDurationSeconds) .sum(), first.startedAt(), last.endedAt(), @@ -1409,7 +1349,7 @@ public class TachographFileSessionProcessingService { "Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.", "Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.", "Daily/weekly rest candidate intervals are driving interruption intervals longer than the configured minimum rest-period threshold.", - "Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and unknown-coverage metrics computed from vehicle-usage and VU card-absent overlap.", + "Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and card-absent coverage metrics computed from vehicle-usage and VU card-absent overlap.", "Daily/weekly rest candidate coverage intervals also attach begin/end geo evidence from nearby support events for the same driver and boundary-side vehicle identity.", "Boundary geo evidence prefers the nearest matching POSITION event, then PLACE, BORDER_CROSSING, and LOAD_UNLOAD within the configured lookback/lookahead windows.", "If both begin and end geo evidence carry odometer values, geoEvidenceMovementCategory classifies the interval as STATIONARY, MINOR, MOVED, or UNKNOWN.", diff --git a/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl b/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl index 0f1b786..808dccc 100644 --- a/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl +++ b/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl @@ -72,7 +72,7 @@ create schema DailyWeeklyRestCandidateCoverageUnknownResolvedInterval( startedAtEpochSecond long, endedAtEpochSecond long, durationSeconds long, - unknownDurationSeconds long, + cardAbsentDurationSeconds long, previousDrivingSourceIntervalId string, nextDrivingSourceIntervalId string, previousRegistrationKey string, @@ -87,8 +87,7 @@ create schema DailyWeeklyRestCandidateCoverageCardResolvedInterval( startedAtEpochSecond long, endedAtEpochSecond long, durationSeconds long, - cardPresentDurationSeconds long, - unknownDurationSeconds long, + cardAbsentDurationSeconds long, previousDrivingSourceIntervalId string, nextDrivingSourceIntervalId string, previousRegistrationKey string, @@ -103,10 +102,8 @@ create schema DailyWeeklyRestCandidateCoverageInterval( startedAtEpochSecond long, endedAtEpochSecond long, durationSeconds long, - cardPresentDurationSeconds long, - cardPresentCoveragePercent double, - unknownDurationSeconds long, - unknownCoveragePercent double, + cardAbsentDurationSeconds long, + cardAbsentCoveragePercent double, previousDrivingSourceIntervalId string, nextDrivingSourceIntervalId string, previousRegistrationKey string, @@ -277,6 +274,12 @@ create schema DailyWeeklyRestCandidateCoverageFinalizationRequest( endedAtEpochSecond long ); +create schema DailyWeeklyRestCandidateCoverageEmittedKey( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long +); + create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent; create schema VuCardAbsentInterval( @@ -299,10 +302,8 @@ create schema PotentialHomeOvernightStayInterval( startedAtEpochSecond long, endedAtEpochSecond long, durationSeconds long, - cardPresentDurationSeconds long, - cardPresentCoveragePercent double, - unknownDurationSeconds long, - unknownCoveragePercent double, + cardAbsentDurationSeconds long, + cardAbsentCoveragePercent double, previousDrivingSourceIntervalId string, nextDrivingSourceIntervalId string, previousRegistrationKey string, @@ -335,10 +336,8 @@ create schema PotentialInVehicleOvernightStayInterval( startedAtEpochSecond long, endedAtEpochSecond long, durationSeconds long, - cardPresentDurationSeconds long, - cardPresentCoveragePercent double, - unknownDurationSeconds long, - unknownCoveragePercent double, + cardAbsentDurationSeconds long, + cardAbsentCoveragePercent double, previousDrivingSourceIntervalId string, nextDrivingSourceIntervalId string, previousRegistrationKey string, @@ -371,10 +370,8 @@ create schema UnclassifiedDailyWeeklyRestCandidateCoverageInterval( startedAtEpochSecond long, endedAtEpochSecond long, durationSeconds long, - cardPresentDurationSeconds long, - cardPresentCoveragePercent double, - unknownDurationSeconds long, - unknownCoveragePercent double, + cardAbsentDurationSeconds long, + cardAbsentCoveragePercent double, previousDrivingSourceIntervalId string, nextDrivingSourceIntervalId string, previousRegistrationKey string, @@ -409,8 +406,7 @@ create schema PotentialInVehicleTripState( vehicleKey string, containedPotentialInVehicleOvernightStayIntervalCount int, containedPotentialInVehicleOvernightStayDurationSeconds long, - containedCardPresentDurationSeconds long, - containedUnknownDurationSeconds long, + containedCardAbsentDurationSeconds long, firstPotentialInVehicleOvernightStayStartedAtEpochSecond long, lastPotentialInVehicleOvernightStayEndedAtEpochSecond long, firstPreviousDrivingSourceIntervalId string, @@ -427,8 +423,7 @@ create schema PotentialInVehicleTripInterval( vehicleKey string, containedPotentialInVehicleOvernightStayIntervalCount int, containedPotentialInVehicleOvernightStayDurationSeconds long, - containedCardPresentDurationSeconds long, - containedUnknownDurationSeconds long, + containedCardAbsentDurationSeconds long, firstPotentialInVehicleOvernightStayStartedAtEpochSecond long, lastPotentialInVehicleOvernightStayEndedAtEpochSecond long, firstPreviousDrivingSourceIntervalId string, @@ -455,6 +450,7 @@ create window DailyWeeklyRestCandidateEndBoundaryOdometerCandidateWindow#keepall create window DailyWeeklyRestCandidateEndBoundaryOdometerBestScoreWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateEndBoundaryOdometerBestScore; create window DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateEndBoundaryOdometerResolved; create window DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateCoverageCardResolvedInterval; +create window DailyWeeklyRestCandidateCoverageEmittedKeyWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateCoverageEmittedKey; insert into SupportGeoEvidenceWindow select @@ -550,7 +546,7 @@ select then c.endedAtEpochSecond - u.startedAtEpochSecond else u.endedAtEpochSecond - u.startedAtEpochSecond end - ) as unknownDurationSeconds, + ) as cardAbsentDurationSeconds, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.previousRegistrationKey as previousRegistrationKey, @@ -582,7 +578,7 @@ select c.startedAtEpochSecond as startedAtEpochSecond, c.endedAtEpochSecond as endedAtEpochSecond, c.durationSeconds as durationSeconds, - 0L as unknownDurationSeconds, + 0L as cardAbsentDurationSeconds, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.previousRegistrationKey as previousRegistrationKey, @@ -604,65 +600,14 @@ select c.startedAtEpochSecond as startedAtEpochSecond, c.endedAtEpochSecond as endedAtEpochSecond, c.durationSeconds as durationSeconds, - sum( - case - when v.startedAtEpochSecond <= c.startedAtEpochSecond and (v.endedAtEpochSecond is null or v.endedAtEpochSecond >= c.endedAtEpochSecond) - then c.durationSeconds - when v.startedAtEpochSecond <= c.startedAtEpochSecond - then v.endedAtEpochSecond - c.startedAtEpochSecond - when v.endedAtEpochSecond is null or v.endedAtEpochSecond >= c.endedAtEpochSecond - then c.endedAtEpochSecond - v.startedAtEpochSecond - else v.endedAtEpochSecond - v.startedAtEpochSecond - end - ) as cardPresentDurationSeconds, - c.unknownDurationSeconds as unknownDurationSeconds, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.previousRegistrationKey as previousRegistrationKey, c.nextRegistrationKey as nextRegistrationKey, c.previousVehicleKey as previousVehicleKey, c.nextVehicleKey as nextVehicleKey -from DailyWeeklyRestCandidateCoverageUnknownResolvedInterval as c unidirectional, - TachographVehicleUsageIntervalInputEvent#keepall as v -where v.driverKey = c.driverKey - and v.startedAtEpochSecond < c.endedAtEpochSecond - and (v.endedAtEpochSecond is null or v.endedAtEpochSecond > c.startedAtEpochSecond) -group by - c.sessionId, - c.driverKey, - c.startedAtEpochSecond, - c.endedAtEpochSecond, - c.durationSeconds, - c.unknownDurationSeconds, - c.previousDrivingSourceIntervalId, - c.nextDrivingSourceIntervalId, - c.previousRegistrationKey, - c.nextRegistrationKey, - c.previousVehicleKey, - c.nextVehicleKey; - -insert into DailyWeeklyRestCandidateCoverageCardResolvedInterval -select - c.sessionId as sessionId, - c.driverKey as driverKey, - c.startedAtEpochSecond as startedAtEpochSecond, - c.endedAtEpochSecond as endedAtEpochSecond, - c.durationSeconds as durationSeconds, - 0L as cardPresentDurationSeconds, - c.unknownDurationSeconds as unknownDurationSeconds, - c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, - c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, - c.previousRegistrationKey as previousRegistrationKey, - c.nextRegistrationKey as nextRegistrationKey, - c.previousVehicleKey as previousVehicleKey, - c.nextVehicleKey as nextVehicleKey -from DailyWeeklyRestCandidateCoverageUnknownResolvedInterval as c -where not exists ( - select * from TachographVehicleUsageIntervalInputEvent#keepall as v - where v.driverKey = c.driverKey - and v.startedAtEpochSecond < c.endedAtEpochSecond - and (v.endedAtEpochSecond is null or v.endedAtEpochSecond > c.startedAtEpochSecond) -); +from DailyWeeklyRestCandidateCoverageUnknownResolvedInterval as c; insert into DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow select * @@ -1313,10 +1258,8 @@ select c.startedAtEpochSecond as startedAtEpochSecond, c.endedAtEpochSecond as endedAtEpochSecond, c.durationSeconds as durationSeconds, - c.cardPresentDurationSeconds as cardPresentDurationSeconds, - (c.cardPresentDurationSeconds * 100.0d) / c.durationSeconds as cardPresentCoveragePercent, - c.unknownDurationSeconds as unknownDurationSeconds, - (c.unknownDurationSeconds * 100.0d) / c.durationSeconds as unknownCoveragePercent, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, + (c.cardAbsentDurationSeconds * 100.0d) / c.durationSeconds as cardAbsentCoveragePercent, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.previousRegistrationKey as previousRegistrationKey, @@ -1517,7 +1460,20 @@ from DailyWeeklyRestCandidateCoverageFinalizationRequest as request unidirection DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow as c where c.driverKey = request.driverKey and c.startedAtEpochSecond = request.startedAtEpochSecond - and c.endedAtEpochSecond = request.endedAtEpochSecond; + and c.endedAtEpochSecond = request.endedAtEpochSecond + and not exists ( + select * from DailyWeeklyRestCandidateCoverageEmittedKeyWindow as emitted + where emitted.driverKey = request.driverKey + and emitted.startedAtEpochSecond = request.startedAtEpochSecond + and emitted.endedAtEpochSecond = request.endedAtEpochSecond + ); + +insert into DailyWeeklyRestCandidateCoverageEmittedKeyWindow +select + driverKey, + startedAtEpochSecond, + endedAtEpochSecond +from DailyWeeklyRestCandidateCoverageInterval; context PerDriver create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent; @@ -1561,10 +1517,8 @@ select c.startedAtEpochSecond as startedAtEpochSecond, c.endedAtEpochSecond as endedAtEpochSecond, c.durationSeconds as durationSeconds, - c.cardPresentDurationSeconds as cardPresentDurationSeconds, - c.cardPresentCoveragePercent as cardPresentCoveragePercent, - c.unknownDurationSeconds as unknownDurationSeconds, - c.unknownCoveragePercent as unknownCoveragePercent, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, + c.cardAbsentCoveragePercent as cardAbsentCoveragePercent, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.previousRegistrationKey as previousRegistrationKey, @@ -1593,7 +1547,7 @@ from DailyWeeklyRestCandidateCoverageInterval as c where c.previousRegistrationKey is not null and c.nextRegistrationKey is not null and c.previousRegistrationKey != c.nextRegistrationKey - and c.unknownDurationSeconds * 100L >= c.durationSeconds * 95L; + and c.cardAbsentDurationSeconds * 100L >= c.durationSeconds * 95L; insert into PotentialInVehicleOvernightStayInterval select @@ -1602,10 +1556,8 @@ select c.startedAtEpochSecond as startedAtEpochSecond, c.endedAtEpochSecond as endedAtEpochSecond, c.durationSeconds as durationSeconds, - c.cardPresentDurationSeconds as cardPresentDurationSeconds, - c.cardPresentCoveragePercent as cardPresentCoveragePercent, - c.unknownDurationSeconds as unknownDurationSeconds, - c.unknownCoveragePercent as unknownCoveragePercent, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, + c.cardAbsentCoveragePercent as cardAbsentCoveragePercent, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.previousRegistrationKey as previousRegistrationKey, @@ -1634,7 +1586,7 @@ from DailyWeeklyRestCandidateCoverageInterval as c where c.previousRegistrationKey is not null and c.nextRegistrationKey is not null and c.previousRegistrationKey = c.nextRegistrationKey - and c.cardPresentDurationSeconds >= c.durationSeconds; + and c.cardAbsentDurationSeconds = 0L; insert into UnclassifiedDailyWeeklyRestCandidateCoverageInterval select @@ -1643,10 +1595,8 @@ select c.startedAtEpochSecond as startedAtEpochSecond, c.endedAtEpochSecond as endedAtEpochSecond, c.durationSeconds as durationSeconds, - c.cardPresentDurationSeconds as cardPresentDurationSeconds, - c.cardPresentCoveragePercent as cardPresentCoveragePercent, - c.unknownDurationSeconds as unknownDurationSeconds, - c.unknownCoveragePercent as unknownCoveragePercent, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, + c.cardAbsentCoveragePercent as cardAbsentCoveragePercent, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.previousRegistrationKey as previousRegistrationKey, @@ -1676,13 +1626,13 @@ where not ( c.previousRegistrationKey is not null and c.nextRegistrationKey is not null and c.previousRegistrationKey != c.nextRegistrationKey - and c.unknownDurationSeconds * 100L >= c.durationSeconds * 95L + and c.cardAbsentDurationSeconds * 100L >= c.durationSeconds * 95L ) and not ( c.previousRegistrationKey is not null and c.nextRegistrationKey is not null and c.previousRegistrationKey = c.nextRegistrationKey - and c.cardPresentDurationSeconds >= c.durationSeconds + and c.cardAbsentDurationSeconds = 0L ); @Priority(40) @@ -1698,8 +1648,7 @@ select s.vehicleKey as vehicleKey, s.containedPotentialInVehicleOvernightStayIntervalCount as containedPotentialInVehicleOvernightStayIntervalCount, s.containedPotentialInVehicleOvernightStayDurationSeconds as containedPotentialInVehicleOvernightStayDurationSeconds, - s.containedCardPresentDurationSeconds as containedCardPresentDurationSeconds, - s.containedUnknownDurationSeconds as containedUnknownDurationSeconds, + s.containedCardAbsentDurationSeconds as containedCardAbsentDurationSeconds, s.firstPotentialInVehicleOvernightStayStartedAtEpochSecond as firstPotentialInVehicleOvernightStayStartedAtEpochSecond, s.lastPotentialInVehicleOvernightStayEndedAtEpochSecond as lastPotentialInVehicleOvernightStayEndedAtEpochSecond, s.firstPreviousDrivingSourceIntervalId as firstPreviousDrivingSourceIntervalId, @@ -1712,7 +1661,7 @@ where s.driverKey = c.driverKey c.previousRegistrationKey is not null and c.nextRegistrationKey is not null and c.previousRegistrationKey = c.nextRegistrationKey - and c.cardPresentDurationSeconds >= c.durationSeconds + and c.cardAbsentDurationSeconds = 0L ) or s.registrationKey != c.previousRegistrationKey or ( @@ -1730,7 +1679,7 @@ where s.driverKey = c.driverKey c.previousRegistrationKey is not null and c.nextRegistrationKey is not null and c.previousRegistrationKey = c.nextRegistrationKey - and c.cardPresentDurationSeconds >= c.durationSeconds + and c.cardAbsentDurationSeconds = 0L ); @Priority(30) @@ -1744,8 +1693,7 @@ select s.vehicleKey as vehicleKey, s.containedPotentialInVehicleOvernightStayIntervalCount + 1 as containedPotentialInVehicleOvernightStayIntervalCount, s.containedPotentialInVehicleOvernightStayDurationSeconds + c.durationSeconds as containedPotentialInVehicleOvernightStayDurationSeconds, - s.containedCardPresentDurationSeconds + c.cardPresentDurationSeconds as containedCardPresentDurationSeconds, - s.containedUnknownDurationSeconds + c.unknownDurationSeconds as containedUnknownDurationSeconds, + s.containedCardAbsentDurationSeconds + c.cardAbsentDurationSeconds as containedCardAbsentDurationSeconds, s.firstPotentialInVehicleOvernightStayStartedAtEpochSecond as firstPotentialInVehicleOvernightStayStartedAtEpochSecond, c.endedAtEpochSecond as lastPotentialInVehicleOvernightStayEndedAtEpochSecond, s.firstPreviousDrivingSourceIntervalId as firstPreviousDrivingSourceIntervalId, @@ -1755,7 +1703,7 @@ where s.driverKey = c.driverKey and c.previousRegistrationKey is not null and c.nextRegistrationKey is not null and c.previousRegistrationKey = c.nextRegistrationKey - and c.cardPresentDurationSeconds >= c.durationSeconds + and c.cardAbsentDurationSeconds = 0L and s.registrationKey = c.previousRegistrationKey and ( s.vehicleKey is null @@ -1777,8 +1725,7 @@ select end as vehicleKey, 1 as containedPotentialInVehicleOvernightStayIntervalCount, c.durationSeconds as containedPotentialInVehicleOvernightStayDurationSeconds, - c.cardPresentDurationSeconds as containedCardPresentDurationSeconds, - c.unknownDurationSeconds as containedUnknownDurationSeconds, + c.cardAbsentDurationSeconds as containedCardAbsentDurationSeconds, c.startedAtEpochSecond as firstPotentialInVehicleOvernightStayStartedAtEpochSecond, c.endedAtEpochSecond as lastPotentialInVehicleOvernightStayEndedAtEpochSecond, c.previousDrivingSourceIntervalId as firstPreviousDrivingSourceIntervalId, @@ -1788,7 +1735,7 @@ where priorCoverage.driverKey = c.driverKey and c.previousRegistrationKey is not null and c.nextRegistrationKey is not null and c.previousRegistrationKey = c.nextRegistrationKey - and c.cardPresentDurationSeconds >= c.durationSeconds + and c.cardAbsentDurationSeconds = 0L and not exists ( select * from OpenPotentialInVehicleTripState as s where s.driverKey = c.driverKey diff --git a/src/main/resources/esper/tachograph-potential-home-overnight-stay-interval-events.epl b/src/main/resources/esper/tachograph-potential-home-overnight-stay-interval-events.epl index 85900ea..5175630 100644 --- a/src/main/resources/esper/tachograph-potential-home-overnight-stay-interval-events.epl +++ b/src/main/resources/esper/tachograph-potential-home-overnight-stay-interval-events.epl @@ -15,7 +15,7 @@ select then c.endedAtEpochSecond - u.startedAtEpochSecond else u.endedAtEpochSecond - u.startedAtEpochSecond end - ) as unknownDurationSeconds, + ) as cardAbsentDurationSeconds, (sum( case when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond @@ -26,7 +26,7 @@ select then c.endedAtEpochSecond - u.startedAtEpochSecond else u.endedAtEpochSecond - u.startedAtEpochSecond end - ) * 100.0d) / c.durationSeconds as unknownCoveragePercent, + ) * 100.0d) / c.durationSeconds as cardAbsentCoveragePercent, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.previousRegistrationKey as previousRegistrationKey, diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographCompositeSessionControllerTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographCompositeSessionControllerTest.java new file mode 100644 index 0000000..0bf4fd6 --- /dev/null +++ b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographCompositeSessionControllerTest.java @@ -0,0 +1,154 @@ +package at.procon.eventhub.tachographfilesession.api; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import at.procon.eventhub.dto.DriverCardRefDto; +import at.procon.eventhub.dto.DriverRefDto; +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.EventType; +import at.procon.eventhub.dto.VehicleRefDto; +import at.procon.eventhub.dto.VehicleRegistrationRefDto; +import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeDriverEventsResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeSessionListDriversResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeSessionSummaryDto; +import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverSummaryDto; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInCompositeSessionException; +import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionService; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +class TachographCompositeSessionControllerTest { + + @Test + void createsLoadsListsAndProcessesCompositeSession() throws Exception { + TachographCompositeSessionService service = org.mockito.Mockito.mock(TachographCompositeSessionService.class); + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TachographCompositeSessionController(service)) + .setControllerAdvice(new TachographFileSessionExceptionHandler()) + .build(); + + UUID compositeSessionId = UUID.randomUUID(); + UUID firstSessionId = UUID.randomUUID(); + UUID secondSessionId = UUID.randomUUID(); + TachographFileDriverSummaryDto driver = new TachographFileDriverSummaryDto("12:123", "Muster", "Max", "12", "CARD0000000001", 5, 3); + TachographCompositeSessionSummaryDto summary = new TachographCompositeSessionSummaryDto( + compositeSessionId, + "default", + "fleet-week-1", + List.of(firstSessionId, secondSessionId), + List.of(driver), + Instant.parse("2026-05-22T10:00:00Z") + ); + when(service.createCompositeSession(org.mockito.ArgumentMatchers.any())) + .thenReturn(new CreateTachographCompositeSessionResponse(summary)); + when(service.getCompositeSession(compositeSessionId)).thenReturn(summary); + when(service.listDrivers(compositeSessionId)) + .thenReturn(new TachographCompositeSessionListDriversResponse(compositeSessionId, List.of(driver))); + when(service.getMergedDriverEvents(compositeSessionId, "12:123")) + .thenReturn(new TachographCompositeDriverEventsResponse( + compositeSessionId, + "12:123", + List.of(firstSessionId, secondSessionId), + 1, + List.of(event("EVT-1", OffsetDateTime.parse("2026-05-22T08:00:00Z"), EventType.WORK)) + )); + when(service.getMergedDriverTimeline(compositeSessionId, "12:123")) + .thenReturn(new ResolvedDriverTimeline( + "COMPOSITE_TACHOGRAPH_FILE_SESSION", + OffsetDateTime.parse("2026-05-22T08:00:00Z"), + OffsetDateTime.parse("2026-05-22T10:00:00Z"), + List.of(), + List.of(), + List.of(), + List.of() + )); + + mockMvc.perform(post("/api/eventhub/tachograph-composite-sessions") + .contentType("application/json") + .content(""" + { + "sessionIds": ["%s", "%s"], + "label": "fleet-week-1" + } + """.formatted(firstSessionId, secondSessionId))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.session.compositeSessionId").value(compositeSessionId.toString())) + .andExpect(jsonPath("$.session.memberSessionIds[0]").value(firstSessionId.toString())); + + mockMvc.perform(get("/api/eventhub/tachograph-composite-sessions/{compositeSessionId}", compositeSessionId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.compositeSessionId").value(compositeSessionId.toString())) + .andExpect(jsonPath("$.drivers[0].driverKey").value("12:123")); + + mockMvc.perform(get("/api/eventhub/tachograph-composite-sessions/{compositeSessionId}/drivers", compositeSessionId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.drivers[0].cardVehicleUsageIntervalCount").value(3)); + + mockMvc.perform(get("/api/eventhub/tachograph-composite-sessions/{compositeSessionId}/drivers/{driverKey}/events", + compositeSessionId, "12:123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.eventCount").value(1)) + .andExpect(jsonPath("$.events[0].externalSourceEventId").value("EVT-1")) + .andExpect(jsonPath("$.events[0].occurredAt").value("2026-05-22T08:00:00Z")); + + mockMvc.perform(get("/api/eventhub/tachograph-composite-sessions/{compositeSessionId}/drivers/{driverKey}/timeline", + compositeSessionId, "12:123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sourceKind").value("COMPOSITE_TACHOGRAPH_FILE_SESSION")) + .andExpect(jsonPath("$.loadedFrom").value("2026-05-22T08:00:00Z")); + } + + @Test + void returnsNotFoundWhenDriverIsMissingInCompositeSession() throws Exception { + TachographCompositeSessionService service = org.mockito.Mockito.mock(TachographCompositeSessionService.class); + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TachographCompositeSessionController(service)) + .setControllerAdvice(new TachographFileSessionExceptionHandler()) + .build(); + + UUID compositeSessionId = UUID.randomUUID(); + when(service.getMergedDriverEvents(eq(compositeSessionId), eq("missing"))) + .thenThrow(new DriverNotFoundInCompositeSessionException(compositeSessionId, "missing")); + + mockMvc.perform(get("/api/eventhub/tachograph-composite-sessions/{compositeSessionId}/drivers/{driverKey}/events", + compositeSessionId, "missing")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value( + "Driver 'missing' was not found in tachograph composite session '%s'.".formatted(compositeSessionId) + )); + } + + private static EventHubEventDto event(String externalSourceEventId, OffsetDateTime occurredAt, EventType eventType) { + return new EventHubEventDto( + null, + externalSourceEventId, + new DriverRefDto(null, new DriverCardRefDto("12", 12, "CARD0000000001")), + new VehicleRefDto(null, "VIN00000000000001", null, new VehicleRegistrationRefDto("12", 12, "W-12345A")), + occurredAt, + null, + null, + EventDomain.DRIVER_ACTIVITY, + eventType, + EventLifecycle.START, + null, + null, + null, + null, + null, + false, + null + ); + } +} diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java index 6ea091e..bf83b93 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java @@ -178,8 +178,6 @@ class TachographFileSessionControllerTest { OffsetDateTime.parse("2026-05-12T10:00:00Z"), OffsetDateTime.parse("2026-05-12T22:00:00Z"), 43_200L, - 0L, - 0.0d, 43_200L, 100.0d, "ACT-2", @@ -216,8 +214,6 @@ class TachographFileSessionControllerTest { OffsetDateTime.parse("2026-05-12T10:00:00Z"), OffsetDateTime.parse("2026-05-12T22:00:00Z"), 43_200L, - 0L, - 0.0d, 43_200L, 100.0d, "ACT-2", diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java index 4bfc480..949459b 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java @@ -563,8 +563,8 @@ class DriverTimelineBuilderTest { assertThat(intervals.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); assertThat(intervals.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z")); assertThat(intervals.get(0).durationSeconds()).isEqualTo(50_400L); - assertThat(intervals.get(0).unknownDurationSeconds()).isEqualTo(50_399L); - assertThat(intervals.get(0).unknownCoveragePercent()).isGreaterThan(99.9d); + assertThat(intervals.get(0).cardAbsentDurationSeconds()).isEqualTo(50_399L); + assertThat(intervals.get(0).cardAbsentCoveragePercent()).isGreaterThan(99.9d); assertThat(intervals.get(0).previousDrivingSourceIntervalId()).isEqualTo("ACT-1"); assertThat(intervals.get(0).nextDrivingSourceIntervalId()).isEqualTo("ACT-2"); } diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java index fd30572..d783606 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java @@ -128,8 +128,7 @@ class DriverTimelineReusableProjectionBuilderTest { assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1); TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent coverageInterval = reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0); - assertThat(coverageInterval.unknownCoveragePercent()).isCloseTo(99.998d, org.assertj.core.data.Offset.offset(0.001d)); - assertThat(coverageInterval.cardPresentCoveragePercent()).isEqualTo(0.0d); + assertThat(coverageInterval.cardAbsentCoveragePercent()).isCloseTo(99.998d, org.assertj.core.data.Offset.offset(0.001d)); assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).containsExactlyElementsOf(legacyDrivingInterruptionVehicleChanges); assertThat(reusableBundle.vuCardAbsentIntervals()).containsExactlyElementsOf(legacyVuCardAbsentIntervals); assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).hasSize(1); @@ -137,8 +136,8 @@ class DriverTimelineReusableProjectionBuilderTest { .isEqualTo(legacyPotentialHomeOvernightStays.get(0).startedAt()); assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).endedAt()) .isEqualTo(legacyPotentialHomeOvernightStays.get(0).endedAt()); - assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).unknownCoveragePercent()) - .isEqualTo(legacyPotentialHomeOvernightStays.get(0).unknownCoveragePercent()); + assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).cardAbsentCoveragePercent()) + .isEqualTo(legacyPotentialHomeOvernightStays.get(0).cardAbsentCoveragePercent()); assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).beginBoundaryOdometerKm()) .isEqualTo(200L); assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).endBoundaryOdometerKm()) @@ -266,9 +265,7 @@ class DriverTimelineReusableProjectionBuilderTest { assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).isEmpty(); assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1); - assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()) - .isEqualTo(100.0d); - assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent()) + assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentCoveragePercent()) .isEqualTo(0.0d); assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginGeoEventId()) .isEqualTo("SUP-1"); @@ -296,8 +293,6 @@ class DriverTimelineReusableProjectionBuilderTest { .isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).endedAt()) .isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z")); - assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).cardPresentCoveragePercent()) - .isEqualTo(100.0d); assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).beginGeoEventId()) .isEqualTo("SUP-1"); assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).beginGeoEvent()) diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionServiceTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionServiceTest.java new file mode 100644 index 0000000..87e8511 --- /dev/null +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionServiceTest.java @@ -0,0 +1,306 @@ +package at.procon.eventhub.tachographfilesession.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import at.procon.eventhub.dto.DriverCardRefDto; +import at.procon.eventhub.dto.DriverRefDto; +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.EventType; +import at.procon.eventhub.dto.VehicleRefDto; +import at.procon.eventhub.dto.VehicleRegistrationRefDto; +import at.procon.eventhub.processing.service.UnifiedEventTimelineReconstructor; +import at.procon.eventhub.service.EventAcquisitionRecordKeyService; +import at.procon.eventhub.service.EventHubEventSorter; +import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionRequest; +import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionResponse; +import at.procon.eventhub.tachographfilesession.dto.TachographCompositeDriverEventsResponse; +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriver; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard; +import at.procon.eventhub.tachographfilesession.model.ExtractionStats; +import at.procon.eventhub.tachographfilesession.model.ExtractionWarning; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata; +import java.time.Instant; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class TachographCompositeSessionServiceTest { + + @Test + void createsCompositeSessionAndAggregatesDriversAcrossMemberSessions() { + TachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository(); + TachographFileSessionRepository fileSessionRepository = Mockito.mock(TachographFileSessionRepository.class); + DriverTimelineEventBuilder eventBuilder = Mockito.mock(DriverTimelineEventBuilder.class); + UnifiedEventTimelineReconstructor reconstructor = Mockito.mock(UnifiedEventTimelineReconstructor.class); + TachographCompositeSessionService service = new TachographCompositeSessionService( + compositeRepository, + fileSessionRepository, + eventBuilder, + reconstructor, + new EventAcquisitionRecordKeyService(), + new EventHubEventSorter() + ); + + UUID firstSessionId = UUID.randomUUID(); + UUID secondSessionId = UUID.randomUUID(); + TachographFileSession first = session( + firstSessionId, + driver("12:123", "Muster", "Max", "12", "CARD0000000001", 2, 1, List.of(new ExtractionWarning("A", "a", "a"))), + driver("13:456", "Doe", "Jane", "13", "CARD0000000002", 1, 1, List.of()) + ); + TachographFileSession second = session( + secondSessionId, + driver("12:123", "Muster", "Max", "12", "CARD0000000001", 3, 2, List.of()), + driver("14:789", "Smith", "John", "14", "CARD0000000003", 4, 5, List.of()) + ); + when(fileSessionRepository.find(firstSessionId)).thenReturn(java.util.Optional.of(first)); + when(fileSessionRepository.find(secondSessionId)).thenReturn(java.util.Optional.of(second)); + + CreateTachographCompositeSessionResponse response = service.createCompositeSession( + new CreateTachographCompositeSessionRequest(List.of(firstSessionId, secondSessionId, firstSessionId), "fleet-week-1") + ); + + assertThat(response.session().tenantKey()).isEqualTo("default"); + assertThat(response.session().label()).isEqualTo("fleet-week-1"); + assertThat(response.session().memberSessionIds()).containsExactly(firstSessionId, secondSessionId); + assertThat(response.session().drivers()).hasSize(3); + assertThat(response.session().drivers()) + .filteredOn(driver -> driver.driverKey().equals("12:123")) + .singleElement() + .satisfies(driverSummary -> { + assertThat(driverSummary.activityIntervalCount()).isEqualTo(5); + assertThat(driverSummary.cardVehicleUsageIntervalCount()).isEqualTo(3); + assertThat(driverSummary.surname()).isEqualTo("Muster"); + assertThat(driverSummary.firstNames()).isEqualTo("Max"); + assertThat(driverSummary.cardNumber()).isEqualTo("CARD0000000001"); + }); + } + + @Test + void mergesDriverEventsAcrossSessionsAndDeduplicatesBySemanticSignature() { + TachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository(); + TachographFileSessionRepository fileSessionRepository = Mockito.mock(TachographFileSessionRepository.class); + DriverTimelineEventBuilder eventBuilder = Mockito.mock(DriverTimelineEventBuilder.class); + UnifiedEventTimelineReconstructor reconstructor = Mockito.mock(UnifiedEventTimelineReconstructor.class); + TachographCompositeSessionService service = new TachographCompositeSessionService( + compositeRepository, + fileSessionRepository, + eventBuilder, + reconstructor, + new EventAcquisitionRecordKeyService(), + new EventHubEventSorter() + ); + + UUID firstSessionId = UUID.randomUUID(); + UUID secondSessionId = UUID.randomUUID(); + TachographFileSession first = session(firstSessionId, driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of())); + TachographFileSession second = session(secondSessionId, driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of())); + when(fileSessionRepository.find(firstSessionId)).thenReturn(java.util.Optional.of(first)); + when(fileSessionRepository.find(secondSessionId)).thenReturn(java.util.Optional.of(second)); + + CreateTachographCompositeSessionResponse created = service.createCompositeSession( + new CreateTachographCompositeSessionRequest(List.of(firstSessionId, secondSessionId), "driver-merge") + ); + + DriverExtractionSession firstDriver = first.driversByKey().get("12:123"); + DriverExtractionSession secondDriver = second.driversByKey().get("12:123"); + OffsetDateTime firstTimestamp = OffsetDateTime.parse("2026-05-22T08:00:00Z"); + OffsetDateTime secondTimestamp = OffsetDateTime.parse("2026-05-22T09:00:00Z"); + when(eventBuilder.buildEvents(first, firstDriver)).thenReturn(List.of( + event("duplicate-a", firstTimestamp, EventType.WORK), + event("unique-a", secondTimestamp, EventType.DRIVE) + )); + when(eventBuilder.buildEvents(second, secondDriver)).thenReturn(List.of( + event("duplicate-b", firstTimestamp, EventType.WORK), + event("unique-b", secondTimestamp.plusMinutes(30), EventType.BREAK_REST) + )); + + TachographCompositeDriverEventsResponse response = + service.getMergedDriverEvents(created.session().compositeSessionId(), "12:123"); + + assertThat(response.sourceSessionIds()).containsExactly(firstSessionId, secondSessionId); + assertThat(response.eventCount()).isEqualTo(3); + assertThat(response.events()).extracting(EventHubEventDto::externalSourceEventId) + .containsExactly("duplicate-a", "unique-a", "unique-b"); + } + + @Test + void reconstructsMergedDriverTimelineFromMergedEventsAndWarnings() { + TachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository(); + TachographFileSessionRepository fileSessionRepository = Mockito.mock(TachographFileSessionRepository.class); + DriverTimelineEventBuilder eventBuilder = Mockito.mock(DriverTimelineEventBuilder.class); + UnifiedEventTimelineReconstructor reconstructor = Mockito.mock(UnifiedEventTimelineReconstructor.class); + TachographCompositeSessionService service = new TachographCompositeSessionService( + compositeRepository, + fileSessionRepository, + eventBuilder, + reconstructor, + new EventAcquisitionRecordKeyService(), + new EventHubEventSorter() + ); + + UUID firstSessionId = UUID.randomUUID(); + UUID secondSessionId = UUID.randomUUID(); + DriverExtractionSession firstDriver = driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of( + new ExtractionWarning("CARD_GAP", "gap", "p1") + )); + DriverExtractionSession secondDriver = driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of( + new ExtractionWarning("DUPLICATE", "duplicate", "p2") + )); + TachographFileSession first = session(firstSessionId, List.of(new ExtractionWarning("SESSION_A", "warn-a", "s1")), firstDriver); + TachographFileSession second = session(secondSessionId, List.of(new ExtractionWarning("SESSION_B", "warn-b", "s2")), secondDriver); + when(fileSessionRepository.find(firstSessionId)).thenReturn(java.util.Optional.of(first)); + when(fileSessionRepository.find(secondSessionId)).thenReturn(java.util.Optional.of(second)); + + CreateTachographCompositeSessionResponse created = service.createCompositeSession( + new CreateTachographCompositeSessionRequest(List.of(firstSessionId, secondSessionId), null) + ); + + OffsetDateTime occurredAt = OffsetDateTime.parse("2026-05-22T08:00:00Z"); + when(eventBuilder.buildEvents(first, firstDriver)).thenReturn(List.of(event("a", occurredAt, EventType.WORK))); + when(eventBuilder.buildEvents(second, secondDriver)).thenReturn(List.of(event("b", occurredAt.plusHours(1), EventType.DRIVE))); + + ResolvedDriverTimeline expected = new ResolvedDriverTimeline( + "COMPOSITE_TACHOGRAPH_FILE_SESSION", + occurredAt, + occurredAt.plusHours(1), + List.of(), + List.of(), + List.of(), + List.of() + ); + when(reconstructor.reconstruct(any(), eq("12:123"), any(), any(), eq("COMPOSITE_TACHOGRAPH_FILE_SESSION"))) + .thenReturn(expected); + + ResolvedDriverTimeline actual = + service.getMergedDriverTimeline(created.session().compositeSessionId(), "12:123"); + + assertThat(actual).isSameAs(expected); + ArgumentCaptor> eventsCaptor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor> warningsCaptor = ArgumentCaptor.forClass(List.class); + verify(reconstructor).reconstruct( + eq(created.session().compositeSessionId()), + eq("12:123"), + eventsCaptor.capture(), + warningsCaptor.capture(), + eq("COMPOSITE_TACHOGRAPH_FILE_SESSION") + ); + assertThat(eventsCaptor.getValue()).extracting(EventHubEventDto::externalSourceEventId) + .containsExactly("a", "b"); + assertThat(warningsCaptor.getValue()).containsExactly( + new ExtractionWarning("SESSION_A", "warn-a", "s1"), + new ExtractionWarning("CARD_GAP", "gap", "p1"), + new ExtractionWarning("SESSION_B", "warn-b", "s2"), + new ExtractionWarning("DUPLICATE", "duplicate", "p2") + ); + } + + @Test + void rejectsMissingDriverInCompositeSession() { + TachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository(); + TachographFileSessionRepository fileSessionRepository = Mockito.mock(TachographFileSessionRepository.class); + TachographCompositeSessionService service = new TachographCompositeSessionService( + compositeRepository, + fileSessionRepository, + Mockito.mock(DriverTimelineEventBuilder.class), + Mockito.mock(UnifiedEventTimelineReconstructor.class), + new EventAcquisitionRecordKeyService(), + new EventHubEventSorter() + ); + + UUID sessionId = UUID.randomUUID(); + when(fileSessionRepository.find(sessionId)).thenReturn(java.util.Optional.of( + session(sessionId, driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of())) + )); + + CreateTachographCompositeSessionResponse created = service.createCompositeSession( + new CreateTachographCompositeSessionRequest(List.of(sessionId), null) + ); + + assertThatThrownBy(() -> service.getMergedDriverEvents(created.session().compositeSessionId(), "missing")) + .isInstanceOf(DriverNotFoundInCompositeSessionException.class) + .hasMessageContaining("missing"); + } + + private static TachographFileSession session(UUID sessionId, DriverExtractionSession... drivers) { + return session(sessionId, List.of(), drivers); + } + + private static TachographFileSession session( + UUID sessionId, + List warnings, + DriverExtractionSession... drivers + ) { + Map byKey = java.util.Arrays.stream(drivers) + .collect(java.util.stream.Collectors.toMap(DriverExtractionSession::driverKey, driver -> driver)); + return new TachographFileSession( + sessionId, + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "sha", 3, "42", "xml", true, null), + byKey, + new ExtractionStats(byKey.size(), 0, 0, 0, 0, warnings.size()), + warnings, + Instant.parse("2026-05-22T08:00:00Z"), + Instant.parse("2026-05-22T12:00:00Z") + ); + } + + private static DriverExtractionSession driver( + String driverKey, + String surname, + String firstNames, + String cardNation, + String cardNumber, + int activityIntervalCount, + int cardVehicleUsageIntervalCount, + List warnings + ) { + return new DriverExtractionSession( + driverKey, + new ExtractedDriver(driverKey, "SOURCE:" + driverKey, surname, firstNames, LocalDate.parse("1980-01-01"), null, null, null, null), + new ExtractedDriverCard("CARD:" + driverKey, cardNation, cardNumber, cardNumber + "01", null, null, null, null), + List.of(), + List.of(), + java.util.Collections.nCopies(cardVehicleUsageIntervalCount, null), + java.util.Collections.nCopies(activityIntervalCount, null), + List.of(), + warnings + ); + } + + private static EventHubEventDto event(String externalSourceEventId, OffsetDateTime occurredAt, EventType eventType) { + return new EventHubEventDto( + null, + externalSourceEventId, + new DriverRefDto(null, new DriverCardRefDto("12", 12, "CARD0000000001")), + new VehicleRefDto(null, "VIN00000000000001", null, new VehicleRegistrationRefDto("12", 12, "W-12345A")), + occurredAt, + null, + null, + eventType == EventType.DRIVE ? EventDomain.DRIVER_ACTIVITY : EventDomain.DRIVER_ACTIVITY, + eventType, + eventType == EventType.BREAK_REST ? EventLifecycle.END : EventLifecycle.START, + null, + null, + null, + null, + null, + false, + null + ); + } +} diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java index c5e57cf..35bd3aa 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java @@ -391,10 +391,9 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.dailyWeeklyRestCandidateIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z")); assertThat(result.dailyWeeklyRestCandidateIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T23:00:00Z")); assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z")); - assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownDurationSeconds()).isEqualTo(43_200L); - assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent()).isEqualTo(100.0d); - assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(0L); - assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(0.0d); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentDurationSeconds()).isEqualTo(50_399L); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentCoveragePercent()) + .isCloseTo(99.998d, org.assertj.core.data.Offset.offset(0.001d)); assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z")); assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).previousRegistrationKey()).isEqualTo("12:REG-1"); assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2"); @@ -403,10 +402,9 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.potentialInVehicleTripIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z")); assertThat(result.potentialHomeOvernightStayIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T23:00:00Z")); - assertThat(result.potentialHomeOvernightStayIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(0L); - assertThat(result.potentialHomeOvernightStayIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(0.0d); - assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownDurationSeconds()).isEqualTo(43_200L); - assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownCoveragePercent()).isEqualTo(100.0d); + assertThat(result.potentialHomeOvernightStayIntervals().get(0).cardAbsentDurationSeconds()).isEqualTo(50_399L); + assertThat(result.potentialHomeOvernightStayIntervals().get(0).cardAbsentCoveragePercent()) + .isCloseTo(99.998d, org.assertj.core.data.Offset.offset(0.001d)); } @Test @@ -483,15 +481,11 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(1); assertThat(result.potentialInVehicleTripIntervalCount()).isEqualTo(0); - assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(50_400L); - assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(100.0d); - assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownDurationSeconds()).isEqualTo(0L); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentDurationSeconds()).isEqualTo(0L); assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z")); - assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(50_400L); - assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(100.0d); - assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).unknownDurationSeconds()).isEqualTo(0L); - assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).unknownCoveragePercent()).isEqualTo(0.0d); + assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardAbsentDurationSeconds()).isEqualTo(0L); + assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardAbsentCoveragePercent()).isEqualTo(0.0d); } @Test @@ -580,9 +574,7 @@ class TachographFileSessionProcessingServiceTest { .isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).endedAt()) .isEqualTo(OffsetDateTime.parse("2026-05-02T00:30:00Z")); - assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()) - .isLessThan(95.0d); - assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent()) + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentCoveragePercent()) .isLessThan(95.0d); }