diff --git a/src/main/java/at/procon/eventhub/config/EventHubProperties.java b/src/main/java/at/procon/eventhub/config/EventHubProperties.java index 73c209f..acafed7 100644 --- a/src/main/java/at/procon/eventhub/config/EventHubProperties.java +++ b/src/main/java/at/procon/eventhub/config/EventHubProperties.java @@ -356,12 +356,23 @@ public class EventHubProperties { } public static class Processing { + private TimelineInputMode timelineInputMode = TimelineInputMode.INTERVALS; private int operatingSplitIdleHours = 7; private int significantDrivingMinutes = 3; private int minimumRestPeriodMinutes = 720; private int mergeGapSeconds = 0; private int gapDetectionToleranceSeconds = 0; + public TimelineInputMode getTimelineInputMode() { + return timelineInputMode; + } + + public void setTimelineInputMode(TimelineInputMode timelineInputMode) { + if (timelineInputMode != null) { + this.timelineInputMode = timelineInputMode; + } + } + public int getOperatingSplitIdleHours() { return operatingSplitIdleHours; } @@ -403,6 +414,11 @@ public class EventHubProperties { } } + public enum TimelineInputMode { + INTERVALS, + EVENTS + } + public static class LegalRequirements { private static final String DEFAULT_BASE_URL = "https://legalrequirements.services.bytebar.eu/ODataV4/LR"; diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/EventBackedDriverTimelineBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/EventBackedDriverTimelineBuilder.java new file mode 100644 index 0000000..35abf56 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/EventBackedDriverTimelineBuilder.java @@ -0,0 +1,427 @@ +package at.procon.eventhub.tachographfilesession.service; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; +import at.procon.eventhub.tachographfilesession.model.ExtractionWarning; +import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle; +import com.fasterxml.jackson.databind.JsonNode; +import java.math.BigDecimal; +import java.time.OffsetDateTime; +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.Component; + +@Component +public class EventBackedDriverTimelineBuilder { + + private final DriverTimelineEventBuilder eventBuilder; + + public EventBackedDriverTimelineBuilder(DriverTimelineEventBuilder eventBuilder) { + this.eventBuilder = eventBuilder; + } + + public ResolvedDriverTimeline build( + TachographFileSession session, + DriverExtractionSession driverSession + ) { + TachographTimelineEventBundle bundle = eventBuilder.buildEventBundle(session, driverSession); + List activityIntervals = + reconstructActivityIntervals(bundle.activityEvents()); + List vehicleUsageIntervals = + reconstructVehicleUsageIntervals(session.sessionId(), driverSession.driverKey(), bundle.vehicleUsageEvents()); + List supportEvents = + reconstructSupportEvents(bundle.supportEvents()); + List warnings = mergeWarnings(session.warnings(), driverSession.warnings()); + + OffsetDateTime loadedFrom = minTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents); + OffsetDateTime loadedTo = maxTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents); + return new ResolvedDriverTimeline( + resolveSourceKind(session, bundle), + loadedFrom, + loadedTo, + vehicleUsageIntervals, + activityIntervals, + supportEvents, + warnings + ); + } + + private List reconstructActivityIntervals(List activityEvents) { + if (activityEvents == null || activityEvents.isEmpty()) { + return List.of(); + } + Map byIntervalId = new LinkedHashMap<>(); + for (EventHubEventDto event : activityEvents) { + if (!"DRIVER_ACTIVITY".equals(event.eventDomain().name())) { + continue; + } + JsonNode raw = raw(event); + String intervalId = text(raw, "intervalId"); + if (intervalId == null) { + intervalId = text(raw, "sourceRowId"); + } + if (intervalId == null) { + intervalId = event.externalSourceEventId(); + } + String resolvedIntervalId = intervalId; + ActivityAccumulator accumulator = byIntervalId.computeIfAbsent( + resolvedIntervalId, + ignored -> new ActivityAccumulator(resolvedIntervalId) + ); + accumulator.accept(event, raw); + } + List result = new ArrayList<>(byIntervalId.size()); + for (ActivityAccumulator accumulator : byIntervalId.values()) { + ResolvedActivityInterval interval = accumulator.finish(); + if (interval != null) { + result.add(interval); + } + } + result.sort(Comparator.comparing(ResolvedActivityInterval::from) + .thenComparing(ResolvedActivityInterval::to) + .thenComparing(ResolvedActivityInterval::activityType, Comparator.nullsLast(String::compareTo))); + return List.copyOf(result); + } + + private List reconstructVehicleUsageIntervals( + UUID sessionId, + String driverKey, + List vehicleUsageEvents + ) { + if (vehicleUsageEvents == null || vehicleUsageEvents.isEmpty()) { + return List.of(); + } + Map byIntervalId = new LinkedHashMap<>(); + for (EventHubEventDto event : vehicleUsageEvents) { + if (!"DRIVER_CARD".equals(event.eventDomain().name())) { + continue; + } + JsonNode raw = raw(event); + String intervalId = text(raw, "intervalId"); + if (intervalId == null) { + intervalId = text(raw, "sourceRowId"); + } + if (intervalId == null) { + intervalId = event.externalSourceEventId(); + } + String resolvedIntervalId = intervalId; + VehicleUsageAccumulator accumulator = byIntervalId.computeIfAbsent( + resolvedIntervalId, + ignored -> new VehicleUsageAccumulator(sessionId, driverKey, resolvedIntervalId) + ); + accumulator.accept(event, raw); + } + List result = new ArrayList<>(byIntervalId.size()); + for (VehicleUsageAccumulator accumulator : byIntervalId.values()) { + ResolvedVehicleUsageInterval interval = accumulator.finish(); + if (interval != null) { + result.add(interval); + } + } + result.sort(Comparator.comparing(ResolvedVehicleUsageInterval::from) + .thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder()))); + return List.copyOf(result); + } + + private List reconstructSupportEvents(List supportEvents) { + if (supportEvents == null || supportEvents.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(supportEvents.size()); + for (EventHubEventDto event : supportEvents) { + JsonNode raw = raw(event); + String eventId = text(raw, "supportEventId"); + if (eventId == null) { + eventId = event.externalSourceEventId(); + } + BigDecimal latitude = event.position() == null ? null : event.position().latitude(); + BigDecimal longitude = event.position() == null ? null : event.position().longitude(); + result.add(new ExtractedSupportEvent( + eventId, + event.occurredAt(), + event.eventDomain().name(), + text(raw, "supportEventType") == null ? event.eventType().name() : text(raw, "supportEventType"), + text(raw, "slot"), + text(raw, "registrationKey"), + text(raw, "vehicleKey"), + text(raw, "country"), + text(raw, "region"), + latitude, + longitude, + text(raw, "authenticationStatus"), + longValue(raw, "odometerKm"), + text(raw, "code"), + text(raw, "rawRecordPath") + )); + } + result.sort(Comparator.comparing(ExtractedSupportEvent::occurredAt) + .thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo)) + .thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo))); + return List.copyOf(result); + } + + private List mergeWarnings(List sessionWarnings, List driverWarnings) { + LinkedHashSet merged = new LinkedHashSet<>(); + if (sessionWarnings != null) { + merged.addAll(sessionWarnings); + } + if (driverWarnings != null) { + merged.addAll(driverWarnings); + } + return List.copyOf(merged); + } + + private OffsetDateTime minTimestamp( + List activityIntervals, + List vehicleUsageIntervals, + List supportEvents + ) { + OffsetDateTime min = null; + for (ResolvedActivityInterval interval : activityIntervals) { + min = min(min, interval.from()); + } + for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) { + min = min(min, interval.from()); + } + for (ExtractedSupportEvent supportEvent : supportEvents) { + min = min(min, supportEvent.occurredAt()); + } + return min; + } + + private OffsetDateTime maxTimestamp( + List activityIntervals, + List vehicleUsageIntervals, + List supportEvents + ) { + OffsetDateTime max = null; + for (ResolvedActivityInterval interval : activityIntervals) { + max = max(max, interval.to()); + } + for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) { + max = max(max, interval.to()); + } + for (ExtractedSupportEvent supportEvent : supportEvents) { + max = max(max, supportEvent.occurredAt()); + } + return max; + } + + private String resolveSourceKind(TachographFileSession session, TachographTimelineEventBundle bundle) { + for (EventHubEventDto event : bundle.allEvents()) { + JsonNode raw = raw(event); + String sourceKind = text(raw, "sourceKind"); + if (sourceKind != null) { + return sourceKind; + } + if (event.packageInfo() != null && event.packageInfo().eventSource() != null + && event.packageInfo().eventSource().sourceKind() != null) { + return event.packageInfo().eventSource().sourceKind(); + } + } + return session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT"; + } + + private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) { + if (left == null) { + return right; + } + if (right == null) { + return left; + } + return left.isBefore(right) ? left : right; + } + + private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) { + if (left == null) { + return right; + } + if (right == null) { + return left; + } + return left.isAfter(right) ? left : right; + } + + private JsonNode raw(EventHubEventDto event) { + JsonNode payload = event.payload(); + if (payload == null || payload.isMissingNode()) { + return null; + } + JsonNode raw = payload.get("raw"); + return raw == null || raw.isNull() ? payload : raw; + } + + private String text(JsonNode node, String field) { + if (node == null || field == null) { + return null; + } + JsonNode value = node.get(field); + return value == null || value.isNull() ? null : value.asText(null); + } + + private boolean booleanValue(JsonNode node, String field) { + if (node == null || field == null) { + return false; + } + JsonNode value = node.get(field); + return value != null && !value.isNull() && value.asBoolean(false); + } + + private Long longValue(JsonNode node, String field) { + if (node == null || field == null) { + return null; + } + JsonNode value = node.get(field); + if (value == null || value.isNull()) { + return null; + } + return value.isNumber() ? value.asLong() : Long.parseLong(value.asText()); + } + + private List stringList(JsonNode node, String field) { + if (node == null || field == null) { + return List.of(); + } + JsonNode value = node.get(field); + if (value == null || value.isNull() || !value.isArray()) { + return List.of(); + } + List result = new ArrayList<>(); + value.forEach(item -> { + String text = item == null || item.isNull() ? null : item.asText(null); + if (text != null && !text.isBlank()) { + result.add(text); + } + }); + return List.copyOf(result); + } + + private String detailText(EventHubEventDto event, String field) { + if (event.eventDetails() == null || event.eventDetails().attributes() == null) { + return null; + } + JsonNode value = event.eventDetails().attributes().get(field); + return value == null || value.isNull() ? null : value.asText(null); + } + + private String activityType(EventHubEventDto event) { + return switch (event.eventType()) { + case DRIVE -> "DRIVE"; + case WORK -> "WORK"; + case AVAILABILITY -> "AVAILABILITY"; + case BREAK_REST -> "BREAK_REST"; + default -> "UNKNOWN"; + }; + } + + private Long toKilometers(Long meters) { + return meters == null ? null : meters / 1_000L; + } + + private final class ActivityAccumulator { + private final String intervalId; + private OffsetDateTime startedAt; + private OffsetDateTime endedAt; + private EventHubEventDto sample; + private JsonNode raw; + + private ActivityAccumulator(String intervalId) { + this.intervalId = intervalId; + } + + private void accept(EventHubEventDto event, JsonNode raw) { + if (sample == null) { + sample = event; + this.raw = raw; + } + if (event.lifecycle() == at.procon.eventhub.dto.EventLifecycle.START) { + startedAt = event.occurredAt(); + } else if (event.lifecycle() == at.procon.eventhub.dto.EventLifecycle.END) { + endedAt = event.occurredAt(); + } + } + + private ResolvedActivityInterval finish() { + if (sample == null || startedAt == null || endedAt == null || !endedAt.isAfter(startedAt)) { + return null; + } + return new ResolvedActivityInterval( + intervalId, + startedAt, + endedAt, + java.time.Duration.between(startedAt, endedAt).getSeconds(), + activityType(sample), + detailText(sample, "cardSlot"), + detailText(sample, "cardStatus"), + detailText(sample, "drivingStatus"), + text(raw, "registrationKey"), + text(raw, "vehicleKey"), + text(raw, "sourceKind"), + stringList(raw, "sourceRowIds"), + booleanValue(raw, "synthetic"), + booleanValue(raw, "clippedToRequestedPeriod"), + text(raw, "level") == null ? "RAW_INTERVAL" : text(raw, "level") + ); + } + } + + private final class VehicleUsageAccumulator { + private final UUID sessionId; + private final String driverKey; + private final String intervalId; + private OffsetDateTime startedAt; + private OffsetDateTime endedAt; + private Long odometerBeginKm; + private Long odometerEndKm; + private JsonNode raw; + + private VehicleUsageAccumulator(UUID sessionId, String driverKey, String intervalId) { + this.sessionId = sessionId; + this.driverKey = driverKey; + this.intervalId = intervalId; + } + + private void accept(EventHubEventDto event, JsonNode raw) { + if (this.raw == null) { + this.raw = raw; + } + if (event.eventType() == at.procon.eventhub.dto.EventType.CARD_INSERTED) { + startedAt = event.occurredAt(); + odometerBeginKm = toKilometers(event.odometerM()); + } else if (event.eventType() == at.procon.eventhub.dto.EventType.CARD_WITHDRAWN) { + endedAt = event.occurredAt(); + odometerEndKm = toKilometers(event.odometerM()); + } + } + + private ResolvedVehicleUsageInterval finish() { + if (startedAt == null) { + return null; + } + return ResolvedVehicleUsageInterval.resolved( + sessionId, + driverKey, + intervalId, + startedAt, + endedAt, + odometerBeginKm, + odometerEndKm, + text(raw, "registrationKey"), + text(raw, "vehicleKey"), + text(raw, "sourceKind"), + stringList(raw, "sourceRowIds") + ); + } + } +} 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 6718cb4..6168cae 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java @@ -35,17 +35,20 @@ public class TachographFileSessionProcessingService { private final TachographFileSessionRepository repository; private final DriverTimelineBuilder driverTimelineBuilder; private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder; + private final EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder; private final EventHubProperties properties; public TachographFileSessionProcessingService( TachographFileSessionRepository repository, DriverTimelineBuilder driverTimelineBuilder, DriverTimelineReusableProjectionBuilder reusableProjectionBuilder, + EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder, EventHubProperties properties ) { this.repository = repository; this.driverTimelineBuilder = driverTimelineBuilder; this.reusableProjectionBuilder = reusableProjectionBuilder; + this.eventBackedDriverTimelineBuilder = eventBackedDriverTimelineBuilder; this.properties = properties; } @@ -64,7 +67,7 @@ public class TachographFileSessionProcessingService { throw new DriverNotFoundInSessionException(sessionId, driverKey); } - ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver); + ResolvedDriverTimeline timeline = resolveTimeline(session, driver); OffsetDateTime loadedFrom = timeline.loadedFrom(); OffsetDateTime loadedTo = timeline.loadedTo(); OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? loadedFrom : utc(effectiveRequest.occurredFrom()); @@ -146,7 +149,7 @@ public class TachographFileSessionProcessingService { throw new DriverNotFoundInSessionException(sessionId, driverKey); } - ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver); + ResolvedDriverTimeline timeline = resolveTimeline(session, driver); OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? timeline.loadedFrom() : utc(effectiveRequest.occurredFrom()); OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null ? timeline.loadedTo() : utc(effectiveRequest.occurredTo()); if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) { @@ -245,6 +248,17 @@ public class TachographFileSessionProcessingService { ); } + private ResolvedDriverTimeline resolveTimeline( + TachographFileSession session, + DriverExtractionSession driver + ) { + if (properties.getTachographFileSession().getProcessing().getTimelineInputMode() + == EventHubProperties.TimelineInputMode.EVENTS) { + return eventBackedDriverTimelineBuilder.build(session, driver); + } + return driverTimelineBuilder.build(session, driver); + } + private List clipEsperActivityIntervalEvents( List intervals, OffsetDateTime requestedFrom, diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/EventBackedDriverTimelineBuilderTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/EventBackedDriverTimelineBuilderTest.java new file mode 100644 index 0000000..74968ce --- /dev/null +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/EventBackedDriverTimelineBuilderTest.java @@ -0,0 +1,110 @@ +package at.procon.eventhub.tachographfilesession.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import at.procon.eventhub.service.EventDetailsFactory; +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval; +import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriver; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard; +import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; +import at.procon.eventhub.tachographfilesession.model.ExtractedVehicle; +import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration; +import at.procon.eventhub.tachographfilesession.model.ExtractionStats; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class EventBackedDriverTimelineBuilderTest { + + private final DriverTimelineBuilder directBuilder = new DriverTimelineBuilder(); + private final EventBackedDriverTimelineBuilder eventBackedBuilder = + new EventBackedDriverTimelineBuilder( + new IntervalBackedDriverTimelineEventBuilder( + directBuilder, + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ) + ); + + @Test + void reconstructsTimelineFromEvents() { + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), + new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), + List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), + List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), + List.of(new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "vu-1" + )), + List.of(new ExtractedCardActivityInterval( + "ACT-1", + OffsetDateTime.parse("2026-05-01T08:30:00Z"), + OffsetDateTime.parse("2026-05-01T09:00:00Z"), + "DRIVE", + "DRIVER", + "INSERTED", + "SINGLE", + "12:REG-1", + "VIN-1", + "a" + )), + List.of(new ExtractedSupportEvent( + "SUP-1", + OffsetDateTime.parse("2026-05-01T08:45:00Z"), + "POSITION", + "GNSS_ACCUMULATED_DRIVING", + "DRIVER", + "12:REG-1", + "VIN-1", + null, + null, + BigDecimal.valueOf(48.2082), + BigDecimal.valueOf(16.3738), + "AUTHENTIC", + 150L, + null, + "raw-path" + )), + List.of() + ); + TachographFileSession session = new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "42", "b", true, null), + Map.of(driver.driverKey(), driver), + new ExtractionStats(1, 1, 1, 1, 1, 0), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + + ResolvedDriverTimeline direct = directBuilder.build(session, driver); + ResolvedDriverTimeline reconstructed = eventBackedBuilder.build(session, driver); + + assertThat(reconstructed.sourceKind()).isEqualTo(direct.sourceKind()); + assertThat(reconstructed.loadedFrom()).isEqualTo(direct.loadedFrom()); + assertThat(reconstructed.loadedTo()).isEqualTo(direct.loadedTo()); + assertThat(reconstructed.activityIntervals()).containsExactlyElementsOf(direct.activityIntervals()); + assertThat(reconstructed.vehicleUsageIntervals()).containsExactlyElementsOf(direct.vehicleUsageIntervals()); + assertThat(reconstructed.supportEvents()).containsExactlyElementsOf(direct.supportEvents()); + } +} 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 4e00cb5..3737969 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java @@ -3,6 +3,7 @@ package at.procon.eventhub.tachographfilesession.service; import static org.assertj.core.api.Assertions.assertThat; import at.procon.eventhub.config.EventHubProperties; +import at.procon.eventhub.service.EventDetailsFactory; import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest; import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; @@ -19,6 +20,7 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; import java.util.UUID; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; class TachographFileSessionProcessingServiceTest { @@ -32,6 +34,14 @@ class TachographFileSessionProcessingServiceTest { repository, driverTimelineBuilder, new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), + new EventBackedDriverTimelineBuilder( + new IntervalBackedDriverTimelineEventBuilder( + driverTimelineBuilder, + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ) + ), properties ); @@ -106,6 +116,14 @@ class TachographFileSessionProcessingServiceTest { repository, driverTimelineBuilder, new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), + new EventBackedDriverTimelineBuilder( + new IntervalBackedDriverTimelineEventBuilder( + driverTimelineBuilder, + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ) + ), properties ); @@ -191,6 +209,92 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); } + @Test + void canUseEventBackedTimelineModeForEsperProcessing() { + EventHubProperties properties = new EventHubProperties(); + properties.getTachographFileSession().getProcessing().setTimelineInputMode(EventHubProperties.TimelineInputMode.EVENTS); + TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties); + DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder(); + TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( + repository, + driverTimelineBuilder, + new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), + new EventBackedDriverTimelineBuilder( + new IntervalBackedDriverTimelineEventBuilder( + driverTimelineBuilder, + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ) + ), + properties + ); + + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + null, + null, + List.of(), + List.of(), + List.of( + new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T11:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "vu-1" + ), + new ExtractedCardVehicleUsageInterval( + "CVU-2", + OffsetDateTime.parse("2026-05-01T12:00:00Z"), + OffsetDateTime.parse("2026-05-01T13:00:00Z"), + 201L, + 260L, + "12:REG-2", + "VIN-2", + "vu-2" + ) + ), + List.of( + new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:30:00Z"), OffsetDateTime.parse("2026-05-01T09:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"), + new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-01T09:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "WORK", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b"), + new ExtractedCardActivityInterval("ACT-3", OffsetDateTime.parse("2026-05-01T10:00:00Z"), OffsetDateTime.parse("2026-05-01T10:05:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-2", "VIN-2", "c") + ), + List.of(), + List.of() + ); + TachographFileSession session = new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null), + Map.of(driver.driverKey(), driver), + new ExtractionStats(1, 2, 2, 1, 1, 0), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + repository.save(session); + + TachographEsperDriverProcessingResultDto result = service.getEsperDriverProcessingResults( + session.sessionId(), + driver.driverKey(), + new TachographEsperEventsProcessingRequest( + OffsetDateTime.parse("2026-05-01T08:45:00Z"), + OffsetDateTime.parse("2026-05-01T12:30:00Z"), + 3, + 720 + ) + ); + + assertThat(result.activityIntervalCount()).isEqualTo(3); + assertThat(result.drivingIntervalCount()).isEqualTo(2); + assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1); + assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2); + assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); + } + @Test void returnsPotentialHomeOvernightStayIntervalsWhenVuCardAbsentCoversLongDti() { EventHubProperties properties = new EventHubProperties(); @@ -200,6 +304,14 @@ class TachographFileSessionProcessingServiceTest { repository, driverTimelineBuilder, new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), + new EventBackedDriverTimelineBuilder( + new IntervalBackedDriverTimelineEventBuilder( + driverTimelineBuilder, + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ) + ), properties ); @@ -283,6 +395,14 @@ class TachographFileSessionProcessingServiceTest { repository, driverTimelineBuilder, new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), + new EventBackedDriverTimelineBuilder( + new IntervalBackedDriverTimelineEventBuilder( + driverTimelineBuilder, + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ) + ), properties );