From 8f5e51a5c197278df37202895b8b8e48325f0873 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Wed, 20 May 2026 12:00:31 +0200 Subject: [PATCH] Add unified vehicle event sources --- .../EventHubEventReadRepository.java | 90 ++++++++-- .../model/UnifiedVehicleEventsRequest.java | 119 ++++++++++++ ...TachographDbUnifiedVehicleEventSource.java | 30 ++++ ...hFileSessionUnifiedVehicleEventSource.java | 77 ++++++++ .../service/UnifiedVehicleEventSource.java | 12 ++ .../UnifiedVehicleEventSourceService.java | 28 +++ .../YellowFoxDbUnifiedVehicleEventSource.java | 28 +++ .../UnifiedVehicleEventsRequestTest.java | 69 +++++++ .../UnifiedVehicleEventSourceServiceTest.java | 170 ++++++++++++++++++ 9 files changed, 604 insertions(+), 19 deletions(-) create mode 100644 src/main/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequest.java create mode 100644 src/main/java/at/procon/eventhub/processing/service/TachographDbUnifiedVehicleEventSource.java create mode 100644 src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java create mode 100644 src/main/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSource.java create mode 100644 src/main/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSourceService.java create mode 100644 src/main/java/at/procon/eventhub/processing/service/YellowFoxDbUnifiedVehicleEventSource.java create mode 100644 src/test/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequestTest.java create mode 100644 src/test/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSourceServiceTest.java diff --git a/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java b/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java index 221d90d..d3f4909 100644 --- a/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java +++ b/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java @@ -18,6 +18,7 @@ import at.procon.eventhub.dto.SourcePackageRefDto; import at.procon.eventhub.dto.VehicleRefDto; import at.procon.eventhub.dto.VehicleRegistrationRefDto; import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest; +import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -48,6 +49,57 @@ public class EventHubEventReadRepository { UnifiedDriverEventsRequest request, String providerKey, List sourceKinds + ) { + return findEvents( + request.tenantKey(), + request.occurredFrom(), + request.occurredTo(), + request.driverSourceEntityId(), + request.driverCardNation(), + request.driverCardNumber(), + request.vehicleSourceEntityId(), + request.vin(), + request.registrationNation(), + request.registrationNumber(), + providerKey, + sourceKinds + ); + } + + public List findEventsByVehicle( + UnifiedVehicleEventsRequest request, + String providerKey, + List sourceKinds + ) { + return findEvents( + request.tenantKey(), + request.occurredFrom(), + request.occurredTo(), + null, + null, + null, + request.vehicleSourceEntityId(), + request.vin(), + request.registrationNation(), + request.registrationNumber(), + providerKey, + sourceKinds + ); + } + + private List findEvents( + String tenantKey, + OffsetDateTime occurredFrom, + OffsetDateTime occurredTo, + String driverSourceEntityId, + String driverCardNation, + String driverCardNumber, + String vehicleSourceEntityId, + String vin, + String registrationNation, + String registrationNumber, + String providerKey, + List sourceKinds ) { StringBuilder sql = new StringBuilder( """ @@ -127,7 +179,7 @@ public class EventHubEventReadRepository { ); List params = new ArrayList<>(); - params.add(request.tenantKey()); + params.add(tenantKey); params.add(providerKey); if (sourceKinds != null && !sourceKinds.isEmpty()) { @@ -141,40 +193,40 @@ public class EventHubEventReadRepository { } sql.append(")"); } - if (request.occurredFrom() != null) { + if (occurredFrom != null) { sql.append(" and event.occurred_at >= ?"); - params.add(request.occurredFrom()); + params.add(occurredFrom); } - if (request.occurredTo() != null) { + if (occurredTo != null) { sql.append(" and event.occurred_at <= ?"); - params.add(request.occurredTo()); + params.add(occurredTo); } - if (request.driverSourceEntityId() != null) { + if (driverSourceEntityId != null) { sql.append(" and driver.source_driver_entity_id = ?"); - params.add(request.driverSourceEntityId()); + params.add(driverSourceEntityId); } - if (request.driverCardNumber() != null) { + if (driverCardNumber != null) { sql.append(" and driver_card.card_number = ?"); - params.add(request.driverCardNumber()); - if (request.driverCardNation() != null) { + params.add(driverCardNumber); + if (driverCardNation != null) { sql.append(" and driver_card.nation = ?"); - params.add(request.driverCardNation()); + params.add(driverCardNation); } } - if (request.vehicleSourceEntityId() != null) { + if (vehicleSourceEntityId != null) { sql.append(" and vehicle.source_vehicle_entity_id = ?"); - params.add(request.vehicleSourceEntityId()); + params.add(vehicleSourceEntityId); } - if (request.vin() != null) { + if (vin != null) { sql.append(" and vehicle.vin = ?"); - params.add(request.vin()); + params.add(vin); } - if (request.registrationNumber() != null) { + if (registrationNumber != null) { sql.append(" and registration.registration_number = ?"); - params.add(request.registrationNumber()); - if (request.registrationNation() != null) { + params.add(registrationNumber); + if (registrationNation != null) { sql.append(" and registration.nation = ?"); - params.add(request.registrationNation()); + params.add(registrationNation); } } diff --git a/src/main/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequest.java b/src/main/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequest.java new file mode 100644 index 0000000..9338c17 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequest.java @@ -0,0 +1,119 @@ +package at.procon.eventhub.processing.model; + +import java.time.OffsetDateTime; +import java.util.Objects; +import java.util.UUID; + +public record UnifiedVehicleEventsRequest( + UnifiedEventSourceFamily sourceFamily, + UUID sessionId, + String tenantKey, + String vehicleSourceEntityId, + String vin, + String registrationNation, + String registrationNumber, + OffsetDateTime occurredFrom, + OffsetDateTime occurredTo +) { + public UnifiedVehicleEventsRequest { + Objects.requireNonNull(sourceFamily, "sourceFamily must not be null"); + tenantKey = normalize(tenantKey); + vehicleSourceEntityId = normalize(vehicleSourceEntityId); + vin = normalizeUpper(vin); + registrationNation = normalizeUpper(registrationNation); + registrationNumber = normalize(registrationNumber); + if (occurredFrom != null && occurredTo != null && occurredTo.isBefore(occurredFrom)) { + throw new IllegalArgumentException("occurredTo must not be before occurredFrom"); + } + if (sourceFamily == UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION) { + Objects.requireNonNull(sessionId, "sessionId must not be null"); + } else if (tenantKey == null) { + throw new IllegalArgumentException("tenantKey must not be blank"); + } + if (!hasVehicleSelector(vehicleSourceEntityId, vin, registrationNumber)) { + throw new IllegalArgumentException("At least one vehicle selector must be provided."); + } + } + + public static UnifiedVehicleEventsRequest forTachographFileSession( + UUID sessionId, + String vehicleSourceEntityId, + String vin, + String registrationNation, + String registrationNumber, + OffsetDateTime occurredFrom, + OffsetDateTime occurredTo + ) { + return new UnifiedVehicleEventsRequest( + UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION, + sessionId, + null, + vehicleSourceEntityId, + vin, + registrationNation, + registrationNumber, + occurredFrom, + occurredTo + ); + } + + public static UnifiedVehicleEventsRequest forTachographDb( + String tenantKey, + String vehicleSourceEntityId, + String vin, + String registrationNation, + String registrationNumber, + OffsetDateTime occurredFrom, + OffsetDateTime occurredTo + ) { + return new UnifiedVehicleEventsRequest( + UnifiedEventSourceFamily.TACHOGRAPH_DB, + null, + tenantKey, + vehicleSourceEntityId, + vin, + registrationNation, + registrationNumber, + occurredFrom, + occurredTo + ); + } + + public static UnifiedVehicleEventsRequest forYellowFoxDb( + String tenantKey, + String vehicleSourceEntityId, + String vin, + String registrationNation, + String registrationNumber, + OffsetDateTime occurredFrom, + OffsetDateTime occurredTo + ) { + return new UnifiedVehicleEventsRequest( + UnifiedEventSourceFamily.YELLOWFOX_DB, + null, + tenantKey, + vehicleSourceEntityId, + vin, + registrationNation, + registrationNumber, + occurredFrom, + occurredTo + ); + } + + public boolean hasVehicleSelector() { + return hasVehicleSelector(vehicleSourceEntityId, vin, registrationNumber); + } + + private static boolean hasVehicleSelector(String vehicleSourceEntityId, String vin, String registrationNumber) { + return vehicleSourceEntityId != null || vin != null || registrationNumber != null; + } + + private static String normalize(String value) { + return value == null || value.isBlank() ? null : value.trim(); + } + + private static String normalizeUpper(String value) { + return value == null || value.isBlank() ? null : value.trim().toUpperCase(); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/service/TachographDbUnifiedVehicleEventSource.java b/src/main/java/at/procon/eventhub/processing/service/TachographDbUnifiedVehicleEventSource.java new file mode 100644 index 0000000..0a85dc8 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/service/TachographDbUnifiedVehicleEventSource.java @@ -0,0 +1,30 @@ +package at.procon.eventhub.processing.service; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.persistence.EventHubEventReadRepository; +import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; +import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class TachographDbUnifiedVehicleEventSource implements UnifiedVehicleEventSource { + + private static final List SOURCE_KINDS = List.of("DRIVER_CARD", "VEHICLE_UNIT"); + + private final EventHubEventReadRepository repository; + + public TachographDbUnifiedVehicleEventSource(EventHubEventReadRepository repository) { + this.repository = repository; + } + + @Override + public boolean supports(UnifiedVehicleEventsRequest request) { + return request.sourceFamily() == UnifiedEventSourceFamily.TACHOGRAPH_DB; + } + + @Override + public List loadVehicleEvents(UnifiedVehicleEventsRequest request) { + return repository.findEventsByVehicle(request, "TACHOGRAPH", SOURCE_KINDS); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java new file mode 100644 index 0000000..8bc0478 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/service/TachographFileSessionUnifiedVehicleEventSource.java @@ -0,0 +1,77 @@ +package at.procon.eventhub.processing.service; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.dto.VehicleRefDto; +import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; +import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import at.procon.eventhub.tachographfilesession.service.DriverTimelineEventBuilder; +import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException; +import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository; +import java.time.OffsetDateTime; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class TachographFileSessionUnifiedVehicleEventSource implements UnifiedVehicleEventSource { + + private final TachographFileSessionRepository repository; + private final DriverTimelineEventBuilder eventBuilder; + + public TachographFileSessionUnifiedVehicleEventSource( + TachographFileSessionRepository repository, + DriverTimelineEventBuilder eventBuilder + ) { + this.repository = repository; + this.eventBuilder = eventBuilder; + } + + @Override + public boolean supports(UnifiedVehicleEventsRequest request) { + return request.sourceFamily() == UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION; + } + + @Override + public List loadVehicleEvents(UnifiedVehicleEventsRequest request) { + TachographFileSession session = repository.find(request.sessionId()) + .orElseThrow(() -> new TachographFileSessionNotFoundException(request.sessionId())); + return session.driversByKey().values().stream() + .flatMap(driver -> eventBuilder.buildEvents(session, driver).stream()) + .filter(event -> matchesVehicle(event.vehicleRef(), request)) + .filter(event -> withinWindow(event.occurredAt(), request.occurredFrom(), request.occurredTo())) + .distinct() + .toList(); + } + + private boolean matchesVehicle(VehicleRefDto vehicleRef, UnifiedVehicleEventsRequest request) { + if (vehicleRef == null || !vehicleRef.hasAnyReference()) { + return false; + } + if (request.vehicleSourceEntityId() != null + && request.vehicleSourceEntityId().equals(vehicleRef.sourceVehicleEntityId())) { + return true; + } + if (request.vin() != null && request.vin().equals(vehicleRef.vin())) { + return true; + } + return request.registrationNumber() != null + && vehicleRef.vehicleRegistration() != null + && request.registrationNumber().equals(vehicleRef.vehicleRegistration().number()) + && (request.registrationNation() == null + || request.registrationNation().equals(vehicleRef.vehicleRegistration().nation())); + } + + private boolean withinWindow( + OffsetDateTime occurredAt, + OffsetDateTime occurredFrom, + OffsetDateTime occurredTo + ) { + if (occurredAt == null) { + return false; + } + if (occurredFrom != null && occurredAt.isBefore(occurredFrom)) { + return false; + } + return occurredTo == null || !occurredAt.isAfter(occurredTo); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSource.java b/src/main/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSource.java new file mode 100644 index 0000000..0c78104 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSource.java @@ -0,0 +1,12 @@ +package at.procon.eventhub.processing.service; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; +import java.util.List; + +public interface UnifiedVehicleEventSource { + + boolean supports(UnifiedVehicleEventsRequest request); + + List loadVehicleEvents(UnifiedVehicleEventsRequest request); +} diff --git a/src/main/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSourceService.java b/src/main/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSourceService.java new file mode 100644 index 0000000..4e9f71e --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSourceService.java @@ -0,0 +1,28 @@ +package at.procon.eventhub.processing.service; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class UnifiedVehicleEventSourceService { + + private final List eventSources; + + public UnifiedVehicleEventSourceService(List eventSources) { + this.eventSources = List.copyOf(eventSources); + } + + public List loadVehicleEvents(UnifiedVehicleEventsRequest request) { + return eventSources.stream() + .filter(source -> source.supports(request)) + .findFirst() + .orElseThrow(() -> unsupportedSource(request.sourceFamily().name())) + .loadVehicleEvents(request); + } + + private IllegalArgumentException unsupportedSource(String sourceFamily) { + return new IllegalArgumentException("No unified vehicle event source is registered for source family " + sourceFamily + "."); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/service/YellowFoxDbUnifiedVehicleEventSource.java b/src/main/java/at/procon/eventhub/processing/service/YellowFoxDbUnifiedVehicleEventSource.java new file mode 100644 index 0000000..dfe6405 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/service/YellowFoxDbUnifiedVehicleEventSource.java @@ -0,0 +1,28 @@ +package at.procon.eventhub.processing.service; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.persistence.EventHubEventReadRepository; +import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; +import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class YellowFoxDbUnifiedVehicleEventSource implements UnifiedVehicleEventSource { + + private final EventHubEventReadRepository repository; + + public YellowFoxDbUnifiedVehicleEventSource(EventHubEventReadRepository repository) { + this.repository = repository; + } + + @Override + public boolean supports(UnifiedVehicleEventsRequest request) { + return request.sourceFamily() == UnifiedEventSourceFamily.YELLOWFOX_DB; + } + + @Override + public List loadVehicleEvents(UnifiedVehicleEventsRequest request) { + return repository.findEventsByVehicle(request, "YELLOWFOX", List.of("TELEMATICS_PLATFORM")); + } +} diff --git a/src/test/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequestTest.java b/src/test/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequestTest.java new file mode 100644 index 0000000..cc08da1 --- /dev/null +++ b/src/test/java/at/procon/eventhub/processing/model/UnifiedVehicleEventsRequestTest.java @@ -0,0 +1,69 @@ +package at.procon.eventhub.processing.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.OffsetDateTime; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class UnifiedVehicleEventsRequestTest { + + @Test + void buildsFileSessionVehicleRequest() { + UUID sessionId = UUID.randomUUID(); + + UnifiedVehicleEventsRequest request = UnifiedVehicleEventsRequest.forTachographFileSession( + sessionId, + " VEHICLE:1 ", + "wdb123", + "at", + "W-123AB", + OffsetDateTime.parse("2026-05-01T00:00:00Z"), + OffsetDateTime.parse("2026-05-02T00:00:00Z") + ); + + assertThat(request.sourceFamily()).isEqualTo(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION); + assertThat(request.sessionId()).isEqualTo(sessionId); + assertThat(request.vehicleSourceEntityId()).isEqualTo("VEHICLE:1"); + assertThat(request.vin()).isEqualTo("WDB123"); + assertThat(request.registrationNation()).isEqualTo("AT"); + assertThat(request.registrationNumber()).isEqualTo("W-123AB"); + } + + @Test + void buildsDbVehicleRequest() { + UnifiedVehicleEventsRequest request = UnifiedVehicleEventsRequest.forTachographDb( + " default ", + null, + "vin-42", + "de", + "B-123", + null, + null + ); + + assertThat(request.sourceFamily()).isEqualTo(UnifiedEventSourceFamily.TACHOGRAPH_DB); + assertThat(request.tenantKey()).isEqualTo("default"); + assertThat(request.vin()).isEqualTo("VIN-42"); + assertThat(request.registrationNation()).isEqualTo("DE"); + assertThat(request.registrationNumber()).isEqualTo("B-123"); + assertThat(request.hasVehicleSelector()).isTrue(); + } + + @Test + void rejectsRequestWithoutVehicleSelector() { + assertThatThrownBy(() -> new UnifiedVehicleEventsRequest( + UnifiedEventSourceFamily.YELLOWFOX_DB, + null, + "default", + null, + null, + null, + null, + null, + null + )).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("At least one vehicle selector"); + } +} diff --git a/src/test/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSourceServiceTest.java b/src/test/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSourceServiceTest.java new file mode 100644 index 0000000..05f223b --- /dev/null +++ b/src/test/java/at/procon/eventhub/processing/service/UnifiedVehicleEventSourceServiceTest.java @@ -0,0 +1,170 @@ +package at.procon.eventhub.processing.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import at.procon.eventhub.config.EventHubProperties; +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest; +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.TachographFileSession; +import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata; +import at.procon.eventhub.tachographfilesession.service.DriverKeyFactory; +import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder; +import at.procon.eventhub.tachographfilesession.service.InMemoryTachographFileSessionRepository; +import at.procon.eventhub.tachographfilesession.service.IntervalBackedDriverTimelineEventBuilder; +import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository; +import at.procon.eventhub.tachographfilesession.service.VehicleKeyFactory; +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 UnifiedVehicleEventSourceServiceTest { + + @Test + void loadsNormalizedFileSessionVehicleEventsThroughUnifiedService() { + EventHubProperties properties = new EventHubProperties(); + TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties); + DriverTimelineBuilder timelineBuilder = new DriverTimelineBuilder(); + UnifiedVehicleEventSourceService service = new UnifiedVehicleEventSourceService(List.of( + new TachographFileSessionUnifiedVehicleEventSource( + repository, + new IntervalBackedDriverTimelineEventBuilder( + timelineBuilder, + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ) + ) + )); + + DriverExtractionSession driver = driver(); + TachographFileSession session = session(driver); + repository.save(session); + + List events = service.loadVehicleEvents( + UnifiedVehicleEventsRequest.forTachographFileSession( + session.sessionId(), + null, + "VIN-1", + null, + null, + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T09:00:00Z") + ) + ); + + assertThat(events).hasSize(4); + assertThat(events).extracting(event -> event.occurredAt()) + .containsExactly( + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T08:30:00Z"), + OffsetDateTime.parse("2026-05-01T08:45:00Z"), + OffsetDateTime.parse("2026-05-01T09:00:00Z") + ); + assertThat(events).extracting(event -> event.eventDomain()) + .containsExactly( + EventDomain.DRIVER_CARD, + EventDomain.DRIVER_ACTIVITY, + EventDomain.POSITION, + EventDomain.DRIVER_ACTIVITY + ); + assertThat(events.get(0).lifecycle()).isEqualTo(EventLifecycle.INSERT); + assertThat(events.get(1).lifecycle()).isEqualTo(EventLifecycle.START); + assertThat(events.get(3).lifecycle()).isEqualTo(EventLifecycle.END); + } + + private DriverExtractionSession driver() { + return 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", + "POSITION_RECORDED", + "SNAPSHOT", + "DRIVER", + "12:REG-1", + "VIN-1", + null, + null, + null, + null, + null, + BigDecimal.valueOf(48.2082), + BigDecimal.valueOf(16.3738), + "AUTHENTIC", + 150L, + null, + null, + null, + "raw-path" + )), + List.of() + ); + } + + private TachographFileSession session(DriverExtractionSession driver) { + return 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) + ); + } +}