Refine rest candidate derivation chain

This commit is contained in:
trifonovt 2026-05-13 15:22:35 +02:00
parent 3b2f893246
commit 317983eba8
8 changed files with 196 additions and 79 deletions

View File

@ -21,6 +21,7 @@ public record TachographEsperDriverProcessingResultDto(
int drivingIntervalCount,
int drivingInterruptionIntervalCount,
int drivingInterruptionVehicleChangeIntervalCount,
int dailyWeeklyRestCandidateIntervalCount,
int potentialHomeOvernightStayIntervalCount,
int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount,
@ -28,6 +29,7 @@ public record TachographEsperDriverProcessingResultDto(
List<TachographEsperActivityIntervalEvent> drivingIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,

View File

@ -55,6 +55,8 @@ public class DriverTimelineBuilder {
loadResource("esper/tachograph-driving-interruption-interval-events.epl");
private static final String DRIVING_INTERRUPTION_VEHICLE_CHANGE_INTERVAL_EVENTS_EPL =
loadResource("esper/tachograph-driving-interruption-vehicle-change-interval-events.epl");
private static final String DAILY_WEEKLY_REST_CANDIDATE_INTERVAL_EVENTS_EPL_TEMPLATE =
loadResource("esper/tachograph-daily-weekly-rest-candidate-interval-events.epl");
private static final String POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE =
loadResource("esper/tachograph-potential-home-overnight-stay-interval-events.epl");
private static final String VEHICLE_USAGE_INTERVAL_EVENTS_EPL =
@ -209,12 +211,11 @@ public class DriverTimelineBuilder {
}
public List<TachographEsperPotentialHomeOvernightStayIntervalEvent> buildEsperPotentialHomeOvernightStayIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
int minimumRestPeriodMinutes
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals
) {
if (drivingInterruptionIntervals == null
|| drivingInterruptionIntervals.isEmpty()
if (drivingInterruptionVehicleChangeIntervals == null
|| drivingInterruptionVehicleChangeIntervals.isEmpty()
|| vuCardAbsentIntervals == null
|| vuCardAbsentIntervals.isEmpty()) {
return List.of();
@ -223,7 +224,7 @@ public class DriverTimelineBuilder {
executeWithRuntime(
configuration -> {
configuration.getCommon().addEventType(
"TachographDrivingInterruptionIntervalInputEvent",
"TachographDrivingInterruptionVehicleChangeIntervalInputEvent",
drivingInterruptionIntervalInputDefinition()
);
configuration.getCommon().addEventType(
@ -231,7 +232,7 @@ public class DriverTimelineBuilder {
vuCardAbsentIntervalInputDefinition()
);
},
renderPotentialHomeOvernightStayIntervalEventsEpl(minimumRestPeriodMinutes),
POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE,
"potentialHomeOvernightStayIntervals",
newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, result),
runtime -> {
@ -241,6 +242,34 @@ public class DriverTimelineBuilder {
"TachographVuCardAbsentIntervalInputEvent"
);
}
for (TachographEsperDrivingInterruptionIntervalEvent interval : drivingInterruptionVehicleChangeIntervals) {
runtime.getEventService().sendEventMap(
toDrivingInterruptionIntervalInputMap(interval),
"TachographDrivingInterruptionVehicleChangeIntervalInputEvent"
);
}
}
);
return result;
}
public List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDailyWeeklyRestCandidateIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
int minimumRestPeriodMinutes
) {
if (drivingInterruptionIntervals == null || drivingInterruptionIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperDrivingInterruptionIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> configuration.getCommon().addEventType(
"TachographDrivingInterruptionIntervalInputEvent",
drivingInterruptionIntervalInputDefinition()
),
renderDailyWeeklyRestCandidateIntervalEventsEpl(minimumRestPeriodMinutes),
"dailyWeeklyRestCandidateIntervals",
newData -> collectDrivingInterruptionIntervalEventsFromTimestamps(newData, result),
runtime -> {
for (TachographEsperDrivingInterruptionIntervalEvent interval : drivingInterruptionIntervals) {
runtime.getEventService().sendEventMap(
toDrivingInterruptionIntervalInputMap(interval),
@ -1002,10 +1031,10 @@ public class DriverTimelineBuilder {
);
}
private String renderPotentialHomeOvernightStayIntervalEventsEpl(int minimumRestPeriodMinutes) {
private String renderDailyWeeklyRestCandidateIntervalEventsEpl(int minimumRestPeriodMinutes) {
long thresholdSeconds = Math.max(1, minimumRestPeriodMinutes) * 60L;
return POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE.replace(
"${POTENTIAL_HOME_OVERNIGHT_STAY_THRESHOLD_SECONDS}",
return DAILY_WEEKLY_REST_CANDIDATE_INTERVAL_EVENTS_EPL_TEMPLATE.replace(
"${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}",
Long.toString(thresholdSeconds)
);
}

View File

@ -174,11 +174,24 @@ public class TachographFileSessionProcessingService {
requestedFrom,
requestedTo
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
driverTimelineBuilder.buildEsperDailyWeeklyRestCandidateIntervalEvents(
rawDrivingInterruptionIntervals,
minimumRestPeriodMinutes
);
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
clipEsperDrivingInterruptionIntervalEvents(
rawDailyWeeklyRestCandidateIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(
rawDailyWeeklyRestCandidateIntervals
);
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
clipEsperDrivingInterruptionIntervalEvents(
driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(
rawDrivingInterruptionIntervals
),
rawDrivingInterruptionVehicleChangeIntervals,
requestedFrom,
requestedTo
);
@ -187,9 +200,8 @@ public class TachographFileSessionProcessingService {
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
clipEsperPotentialHomeOvernightStayIntervalEvents(
driverTimelineBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents(
rawDrivingInterruptionIntervals,
rawVuCardAbsentIntervals,
minimumRestPeriodMinutes
rawDrivingInterruptionVehicleChangeIntervals,
rawVuCardAbsentIntervals
),
rawVuCardAbsentIntervals,
requestedFrom,
@ -218,6 +230,7 @@ public class TachographFileSessionProcessingService {
drivingIntervals.size(),
drivingInterruptionIntervals.size(),
drivingInterruptionVehicleChangeIntervals.size(),
dailyWeeklyRestCandidateIntervals.size(),
potentialHomeOvernightStayIntervals.size(),
vehicleUsageIntervals.size(),
vuCardAbsentIntervals.size(),
@ -225,6 +238,7 @@ public class TachographFileSessionProcessingService {
drivingIntervals,
drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals,
dailyWeeklyRestCandidateIntervals,
potentialHomeOvernightStayIntervals,
vehicleUsageIntervals,
vuCardAbsentIntervals,
@ -403,7 +417,12 @@ public class TachographFileSessionProcessingService {
return null;
}
long durationSeconds = Duration.between(start, end).getSeconds();
long unknownDurationSeconds = overlapSeconds(start, end, rawVuCardAbsentIntervals, interval.driverKey());
long unknownDurationSeconds = overlapSeconds(
start,
end,
rawVuCardAbsentIntervals,
interval.driverKey()
);
double unknownCoveragePercent = durationSeconds == 0L
? 0.0d
: (unknownDurationSeconds * 100.0d) / durationSeconds;
@ -993,8 +1012,9 @@ public class TachographFileSessionProcessingService {
"This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.",
"Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.",
"Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.",
"Driving interruption vehicle-change intervals are DTI intervals where previousRegistrationKey differs from nextRegistrationKey.",
"Potential home overnight stay intervals are DTI intervals longer than the configured minimum rest-period threshold where VU card-absent overlap covers at least 95% of the DTI.",
"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.",
"Potential home overnight stay intervals are vehicle-change daily/weekly rest candidates where VU card-absent overlap covers at least 95% of the candidate interval.",
"VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver.",
"occurredFrom and occurredTo clip the returned interval projections to the requested UTC time window.",
"Vehicle-usage intervals clear clipped odometer endpoints because boundary odometer values cannot be recomputed safely from the source interval."

View File

@ -0,0 +1,3 @@
@name('dailyWeeklyRestCandidateIntervals')
select *
from TachographDrivingInterruptionIntervalInputEvent(durationSeconds > ${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS});

View File

@ -1,65 +1,65 @@
@name('potentialHomeOvernightStayIntervals')
select
d.sessionId as sessionId,
d.driverKey as driverKey,
d.startedAt as startedAt,
d.endedAt as endedAt,
d.durationSeconds as durationSeconds,
c.sessionId as sessionId,
c.driverKey as driverKey,
c.startedAt as startedAt,
c.endedAt as endedAt,
c.durationSeconds as durationSeconds,
sum(
case
when u.startedAtEpochSecond <= d.startedAtEpochSecond and u.endedAtEpochSecond >= d.endedAtEpochSecond
then d.durationSeconds
when u.startedAtEpochSecond <= d.startedAtEpochSecond
then u.endedAtEpochSecond - d.startedAtEpochSecond
when u.endedAtEpochSecond >= d.endedAtEpochSecond
then d.endedAtEpochSecond - u.startedAtEpochSecond
when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.durationSeconds
when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond
end
) as unknownDurationSeconds,
(sum(
case
when u.startedAtEpochSecond <= d.startedAtEpochSecond and u.endedAtEpochSecond >= d.endedAtEpochSecond
then d.durationSeconds
when u.startedAtEpochSecond <= d.startedAtEpochSecond
then u.endedAtEpochSecond - d.startedAtEpochSecond
when u.endedAtEpochSecond >= d.endedAtEpochSecond
then d.endedAtEpochSecond - u.startedAtEpochSecond
when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.durationSeconds
when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond
end
) * 100.0d) / d.durationSeconds as unknownCoveragePercent,
d.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId,
d.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId,
d.previousRegistrationKey as previousRegistrationKey,
d.nextRegistrationKey as nextRegistrationKey,
d.previousVehicleKey as previousVehicleKey,
d.nextVehicleKey as nextVehicleKey
from TachographDrivingInterruptionIntervalInputEvent(durationSeconds > ${POTENTIAL_HOME_OVERNIGHT_STAY_THRESHOLD_SECONDS}) as d unidirectional,
) * 100.0d) / c.durationSeconds as unknownCoveragePercent,
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 TachographDrivingInterruptionVehicleChangeIntervalInputEvent as c unidirectional,
TachographVuCardAbsentIntervalInputEvent#keepall as u
where u.driverKey = d.driverKey
and u.startedAtEpochSecond < d.endedAtEpochSecond
and u.endedAtEpochSecond > d.startedAtEpochSecond
where u.driverKey = c.driverKey
and u.startedAtEpochSecond < c.endedAtEpochSecond
and u.endedAtEpochSecond > c.startedAtEpochSecond
group by
d.sessionId,
d.driverKey,
d.startedAt,
d.endedAt,
d.startedAtEpochSecond,
d.endedAtEpochSecond,
d.durationSeconds,
d.previousDrivingSourceIntervalId,
d.nextDrivingSourceIntervalId,
d.previousRegistrationKey,
d.nextRegistrationKey,
d.previousVehicleKey,
d.nextVehicleKey
c.sessionId,
c.driverKey,
c.startedAt,
c.endedAt,
c.startedAtEpochSecond,
c.endedAtEpochSecond,
c.durationSeconds,
c.previousDrivingSourceIntervalId,
c.nextDrivingSourceIntervalId,
c.previousRegistrationKey,
c.nextRegistrationKey,
c.previousVehicleKey,
c.nextVehicleKey
having sum(
case
when u.startedAtEpochSecond <= d.startedAtEpochSecond and u.endedAtEpochSecond >= d.endedAtEpochSecond
then d.durationSeconds
when u.startedAtEpochSecond <= d.startedAtEpochSecond
then u.endedAtEpochSecond - d.startedAtEpochSecond
when u.endedAtEpochSecond >= d.endedAtEpochSecond
then d.endedAtEpochSecond - u.startedAtEpochSecond
when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.durationSeconds
when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= c.endedAtEpochSecond
then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond
end
) * 100L >= d.durationSeconds * 95L;
) * 100L >= c.durationSeconds * 95L;

View File

@ -84,6 +84,7 @@ class TachographFileSessionControllerTest {
1,
1,
1,
1,
2,
1,
List.of(new TachographEsperActivityIntervalEvent(
@ -150,6 +151,19 @@ class TachographFileSessionControllerTest {
"VIN-1",
"VIN-2"
)),
List.of(new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-12T10:00:00Z"),
OffsetDateTime.parse("2026-05-12T22:00:00Z"),
43_200L,
"ACT-2",
"ACT-3",
"12:REG-1",
"12:REG-2",
"VIN-1",
"VIN-2"
)),
List.of(new TachographEsperPotentialHomeOvernightStayIntervalEvent(
sessionId,
"12:123",
@ -258,6 +272,7 @@ class TachographFileSessionControllerTest {
.andExpect(jsonPath("$.activityIntervalCount").value(2))
.andExpect(jsonPath("$.drivingInterruptionIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervalCount").value(1))
.andExpect(jsonPath("$.dailyWeeklyRestCandidateIntervalCount").value(1))
.andExpect(jsonPath("$.potentialHomeOvernightStayIntervalCount").value(1))
.andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1"))

View File

@ -378,6 +378,46 @@ class DriverTimelineBuilderTest {
assertThat(absentIntervals.get(0).nextVehicleKey()).isEqualTo("VIN-2");
}
@Test
void buildsDailyWeeklyRestCandidateIntervalsFromDrivingInterruptionIntervals() {
UUID sessionId = UUID.randomUUID();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals = List.of(
new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
OffsetDateTime.parse("2026-05-01T20:00:00Z"),
36_000L,
"ACT-1",
"ACT-2",
"12:REG-1",
"12:REG-1",
"VIN-1",
"VIN-1"
),
new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-02T10:00:00Z"),
OffsetDateTime.parse("2026-05-02T18:00:00Z"),
28_800L,
"ACT-3",
"ACT-4",
"12:REG-1",
"12:REG-1",
"VIN-1",
"VIN-1"
)
);
List<TachographEsperDrivingInterruptionIntervalEvent> candidates =
builder.buildEsperDailyWeeklyRestCandidateIntervalEvents(drivingInterruptionIntervals, 540);
assertThat(candidates).hasSize(1);
assertThat(candidates.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
assertThat(candidates.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T20:00:00Z"));
}
@Test
void buildsEsperDrivingInterruptionIntervalEventsFromSignificantDrivingGaps() {
DriverExtractionSession driver = new DriverExtractionSession(
@ -478,9 +518,9 @@ class DriverTimelineBuilderTest {
"ACT-1",
"ACT-2",
"12:REG-1",
"12:REG-1",
"12:REG-2",
"VIN-1",
"VIN-1"
"VIN-2"
)
);
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = List.of(
@ -499,13 +539,17 @@ class DriverTimelineBuilderTest {
)
);
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
builder.buildEsperDailyWeeklyRestCandidateIntervalEvents(interruptions, 720);
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
builder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(dailyWeeklyRestCandidateIntervals);
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals =
builder.buildEsperPotentialHomeOvernightStayIntervalEvents(
interruptions,
vuCardAbsentIntervals,
720
drivingInterruptionVehicleChangeIntervals,
vuCardAbsentIntervals
);
assertThat(drivingInterruptionVehicleChangeIntervals).hasSize(1);
assertThat(intervals).hasSize(1);
assertThat(intervals.get(0).sessionId()).isEqualTo(sessionId);
assertThat(intervals.get(0).driverKey()).isEqualTo("12:123");

View File

@ -87,6 +87,7 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.drivingIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
@ -169,7 +170,8 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.activityIntervals().get(0).clippedToRequestedPeriod()).isTrue();
assertThat(result.drivingIntervalCount()).isEqualTo(2);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z"));
assertThat(result.drivingInterruptionIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
@ -177,11 +179,6 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.drivingInterruptionIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(result.drivingInterruptionIntervals().get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(result.drivingInterruptionIntervals().get(0).nextVehicleKey()).isEqualTo("VIN-2");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z"));
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).previousRegistrationKey()).isEqualTo("12:REG-1");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).nextVehicleKey()).isEqualTo("VIN-2");
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
assertThat(result.vehicleUsageIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:45:00Z"));
assertThat(result.vehicleUsageIntervals().get(0).odometerBeginKm()).isNull();
@ -230,7 +227,7 @@ class TachographFileSessionProcessingServiceTest {
),
List.of(
new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"),
new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-02T00:00:00Z"), OffsetDateTime.parse("2026-05-02T00:30:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b")
new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-02T00:00:00Z"), OffsetDateTime.parse("2026-05-02T00:30:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-2", "VIN-2", "b")
),
List.of(),
List.of()
@ -257,6 +254,13 @@ class TachographFileSessionProcessingServiceTest {
)
);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(1);
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.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");
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(1);
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"));