Fix VU activity date extraction from downloadTime

This commit is contained in:
trifonovt 2026-06-10 17:45:26 +02:00
parent 8a106c02c5
commit 08b3cbf073
3 changed files with 189 additions and 68 deletions

View File

@ -226,39 +226,34 @@ public class VehicleUnitXmlExtractionService {
VehicleContext vehicleContext,
List<ExtractionWarning> warnings
) {
NodeList dayRecords = nodes(document, "/VehicleUnit/Activities/vuActivityDailyData");
if (dayRecords.getLength() == 0) {
NodeList activitySections = nodes(document, "/VehicleUnit/Activities");
if (activitySections.getLength() == 0) {
return List.of();
}
LocalDate startDate = vehicleContext.defaultActivityStartDate();
if (startDate == null) {
warnings.add(new ExtractionWarning(
"VU_ACTIVITY_DATE_INFERENCE_FAILED",
"Vehicle-unit activity daily data is present but no base date could be inferred from the VU downloadable period.",
"/VehicleUnit/Activities/vuActivityDailyData"
));
return List.of();
}
LocalDate maxDate = vehicleContext.defaultActivityEndDate();
if (maxDate != null) {
LocalDate inferredEndDate = startDate.plusDays(dayRecords.getLength() - 1L);
if (inferredEndDate.isAfter(maxDate)) {
warnings.add(new ExtractionWarning(
"VU_ACTIVITY_DATE_INFERENCE_RANGE",
"Vehicle-unit activity daily records exceed the VU downloadable-period range when mapped by sequence order.",
"/VehicleUnit/Activities/vuActivityDailyData"
));
}
}
List<ExtractedCardActivityInterval> intervals = new ArrayList<>();
int intervalNo = 0;
int fallbackDayIndex = 0;
for (int activityIndex = 0; activityIndex < activitySections.getLength(); activityIndex++) {
Element activities = (Element) activitySections.item(activityIndex);
NodeList dayRecords = nodes(activities, "vuActivityDailyData");
for (int dayIndex = 0; dayIndex < dayRecords.getLength(); dayIndex++) {
Element dayRecord = (Element) dayRecords.item(dayIndex);
LocalDate date = startDate.plusDays(dayIndex);
String dayPath = "/VehicleUnit/Activities[" + (dayIndex + 1) + "]/vuActivityDailyData";
String dayPath = dayRecords.getLength() == 1
? "/VehicleUnit/Activities[" + (activityIndex + 1) + "]/vuActivityDailyData"
: "/VehicleUnit/Activities[" + (activityIndex + 1) + "]/vuActivityDailyData[" + (dayIndex + 1) + "]";
LocalDate date = resolveActivityDate(
activities,
vehicleContext,
activityIndex,
fallbackDayIndex + dayIndex,
dayPath,
warnings
);
if (date == null) {
continue;
}
NodeList changes = nodes(dayRecord, "activityChangeInfos");
List<ActivityChange> parsedChanges = new ArrayList<>();
for (int changeIndex = 0; changeIndex < changes.getLength(); changeIndex++) {
@ -305,11 +300,48 @@ public class VehicleUnitXmlExtractionService {
));
}
}
fallbackDayIndex += dayRecords.getLength();
}
intervals.sort(Comparator.comparing(ExtractedCardActivityInterval::from));
return intervals;
}
private LocalDate resolveActivityDate(
Element activities,
VehicleContext vehicleContext,
int activityIndex,
int fallbackDayIndex,
String dayPath,
List<ExtractionWarning> warnings
) {
OffsetDateTime downloadTime = offsetDateTime(text(activities, "downloadTime"));
if (downloadTime != null) {
return downloadTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDate();
}
LocalDate fallbackDate = vehicleContext.defaultActivityStartDate();
if (fallbackDate == null) {
warnings.add(new ExtractionWarning(
"VU_ACTIVITY_DATE_INFERENCE_FAILED",
"Vehicle-unit activity daily data is present but neither Activities/downloadTime nor the VU downloadable period can provide its date.",
dayPath
));
return null;
}
LocalDate resolvedDate = fallbackDate.plusDays(fallbackDayIndex);
LocalDate maxDate = vehicleContext.defaultActivityEndDate();
if (maxDate != null && resolvedDate.isAfter(maxDate)) {
warnings.add(new ExtractionWarning(
"VU_ACTIVITY_DATE_INFERENCE_RANGE",
"Vehicle-unit activity daily records exceed the VU downloadable-period range when mapped by sequence order.",
"/VehicleUnit/Activities[" + (activityIndex + 1) + "]"
));
}
return resolvedDate;
}
private void assignActivityCoverage(
List<ExtractedCardActivityInterval> vuActivityIntervals,
List<VuCardIwInterval> vuCardIwIntervals,

View File

@ -111,6 +111,35 @@ class VehicleUnitXmlExtractionServiceTest {
assertThat(secondDriver.cardActivityIntervals().get(0).from().toString()).isEqualTo("2026-04-02T07:30Z");
}
@Test
void usesActivitiesDownloadTimeAsVuActivityRecordDate() throws Exception {
TachographFileSession session = service.extract(
new TachographXmlParser.ParsedTachographXml(document(VehicleUnitXmlSamples.vehicleUnitXmlWithActivityDownloadTime()), "VehicleUnit"),
new TachographFileSessionMetadata(
"default",
"legalrequirements-vehicleunit",
"sample-vu",
"sample-vu.ddd",
"abc",
10,
"42",
"def",
false,
null
),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
DriverExtractionSession driver = session.driversByKey().get("12:12345678901200");
assertThat(driver).isNotNull();
assertThat(driver.cardActivityIntervals()).hasSize(2);
assertThat(driver.cardActivityIntervals().get(0).from()).isEqualTo(OffsetDateTime.parse("2026-05-25T08:00:00Z"));
assertThat(driver.cardActivityIntervals().get(1).from()).isEqualTo(OffsetDateTime.parse("2026-05-25T09:00:00Z"));
assertThat(driver.cardActivityIntervals().get(1).to()).isEqualTo(OffsetDateTime.parse("2026-05-26T00:00:00Z"));
assertThat(session.warnings()).isEmpty();
}
@Test
void keepsOpenVuCardUsageRecordWithoutClosingItAtDownloadPeriodEnd() throws Exception {
TachographFileSession session = service.extract(

View File

@ -254,4 +254,64 @@ final class VehicleUnitXmlSamples {
.replace("<timeOfChange>11:00:00Z</timeOfChange>", "<timeOfChange>11:00:00</timeOfChange>")
.replace("<timeOfChange>07:30:00Z</timeOfChange>", "<timeOfChange>07:30:00</timeOfChange>");
}
static String vehicleUnitXmlWithActivityDownloadTime() {
return """
<VehicleUnit>
<Overview>
<vehicleIdentificationNumber>VINVU123456789012</vehicleIdentificationNumber>
<vehicleRegistrationIdentification>
<vehicleRegistrationNation>12</vehicleRegistrationNation>
<vehicleRegistrationNumber><vehicleRegNumber>W-1000V</vehicleRegNumber></vehicleRegistrationNumber>
</vehicleRegistrationIdentification>
<vuDownloadablePeriod>
<minDownloadableTime>2026-05-20T00:00:00Z</minDownloadableTime>
<maxDownloadableTime>2026-05-20T23:59:59Z</maxDownloadableTime>
</vuDownloadablePeriod>
</Overview>
<Activities Generation="3">
<downloadTime>2026-05-25T23:59:59Z</downloadTime>
<odometerValueMidnight>3756</odometerValueMidnight>
<vuCardIWData>
<vuCardIWRecords>
<cardHolderName>
<holderSurname><name>Muster</name></holderSurname>
<holderFirstNames><name>Max</name></holderFirstNames>
</cardHolderName>
<fullCardNumber>
<cardType>DRIVER_CARD</cardType>
<cardIssuingMemberState>12</cardIssuingMemberState>
<cardNumber>
<driverIdentification>123456789012</driverIdentification>
<cardReplacementIndex>0</cardReplacementIndex>
<cardRenewalIndex>0</cardRenewalIndex>
</cardNumber>
<generation>3</generation>
</fullCardNumber>
<cardInsertionTime>2026-05-25T08:00:00Z</cardInsertionTime>
<vehicleOdometerValueAtInsertion>3700</vehicleOdometerValueAtInsertion>
<cardSlotNumber>DRIVER</cardSlotNumber>
</vuCardIWRecords>
</vuCardIWData>
<vuActivityDailyData>
<noOfActivityChanges>2</noOfActivityChanges>
<activityChangeInfos>
<slot>DRIVER</slot>
<drivingStatus>SINGLE</drivingStatus>
<cardStatus>INSERTED</cardStatus>
<activity>WORK</activity>
<timeOfChange>08:00:00Z</timeOfChange>
</activityChangeInfos>
<activityChangeInfos>
<slot>DRIVER</slot>
<drivingStatus>SINGLE</drivingStatus>
<cardStatus>INSERTED</cardStatus>
<activity>DRIVING</activity>
<timeOfChange>09:00:00Z</timeOfChange>
</activityChangeInfos>
</vuActivityDailyData>
</Activities>
</VehicleUnit>
""";
}
}