Fix VU activity date extraction from downloadTime
This commit is contained in:
parent
8a106c02c5
commit
08b3cbf073
|
|
@ -226,39 +226,34 @@ public class VehicleUnitXmlExtractionService {
|
||||||
VehicleContext vehicleContext,
|
VehicleContext vehicleContext,
|
||||||
List<ExtractionWarning> warnings
|
List<ExtractionWarning> warnings
|
||||||
) {
|
) {
|
||||||
NodeList dayRecords = nodes(document, "/VehicleUnit/Activities/vuActivityDailyData");
|
NodeList activitySections = nodes(document, "/VehicleUnit/Activities");
|
||||||
if (dayRecords.getLength() == 0) {
|
if (activitySections.getLength() == 0) {
|
||||||
return List.of();
|
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<>();
|
List<ExtractedCardActivityInterval> intervals = new ArrayList<>();
|
||||||
int intervalNo = 0;
|
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++) {
|
for (int dayIndex = 0; dayIndex < dayRecords.getLength(); dayIndex++) {
|
||||||
Element dayRecord = (Element) dayRecords.item(dayIndex);
|
Element dayRecord = (Element) dayRecords.item(dayIndex);
|
||||||
LocalDate date = startDate.plusDays(dayIndex);
|
String dayPath = dayRecords.getLength() == 1
|
||||||
String dayPath = "/VehicleUnit/Activities[" + (dayIndex + 1) + "]/vuActivityDailyData";
|
? "/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");
|
NodeList changes = nodes(dayRecord, "activityChangeInfos");
|
||||||
List<ActivityChange> parsedChanges = new ArrayList<>();
|
List<ActivityChange> parsedChanges = new ArrayList<>();
|
||||||
for (int changeIndex = 0; changeIndex < changes.getLength(); changeIndex++) {
|
for (int changeIndex = 0; changeIndex < changes.getLength(); changeIndex++) {
|
||||||
|
|
@ -305,11 +300,48 @@ public class VehicleUnitXmlExtractionService {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fallbackDayIndex += dayRecords.getLength();
|
||||||
|
}
|
||||||
|
|
||||||
intervals.sort(Comparator.comparing(ExtractedCardActivityInterval::from));
|
intervals.sort(Comparator.comparing(ExtractedCardActivityInterval::from));
|
||||||
return intervals;
|
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(
|
private void assignActivityCoverage(
|
||||||
List<ExtractedCardActivityInterval> vuActivityIntervals,
|
List<ExtractedCardActivityInterval> vuActivityIntervals,
|
||||||
List<VuCardIwInterval> vuCardIwIntervals,
|
List<VuCardIwInterval> vuCardIwIntervals,
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,35 @@ class VehicleUnitXmlExtractionServiceTest {
|
||||||
assertThat(secondDriver.cardActivityIntervals().get(0).from().toString()).isEqualTo("2026-04-02T07:30Z");
|
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
|
@Test
|
||||||
void keepsOpenVuCardUsageRecordWithoutClosingItAtDownloadPeriodEnd() throws Exception {
|
void keepsOpenVuCardUsageRecordWithoutClosingItAtDownloadPeriodEnd() throws Exception {
|
||||||
TachographFileSession session = service.extract(
|
TachographFileSession session = service.extract(
|
||||||
|
|
|
||||||
|
|
@ -254,4 +254,64 @@ final class VehicleUnitXmlSamples {
|
||||||
.replace("<timeOfChange>11:00:00Z</timeOfChange>", "<timeOfChange>11:00:00</timeOfChange>")
|
.replace("<timeOfChange>11:00:00Z</timeOfChange>", "<timeOfChange>11:00:00</timeOfChange>")
|
||||||
.replace("<timeOfChange>07:30:00Z</timeOfChange>", "<timeOfChange>07:30: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>
|
||||||
|
""";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue