Fix VU interval deduplication and slot normalization

This commit is contained in:
trifonovt 2026-06-11 11:43:20 +02:00
parent ce81bfe868
commit d7ecf52330
3 changed files with 147 additions and 6 deletions

View File

@ -114,7 +114,7 @@ public class VehicleUnitXmlExtractionService {
continue; continue;
} }
builder.vehicleUsageIntervals.add(new ExtractedCardVehicleUsageInterval( builder.addVehicleUsageInterval(new ExtractedCardVehicleUsageInterval(
"VUIW-" + (i + 1), "VUIW-" + (i + 1),
from, from,
to, to,
@ -124,9 +124,9 @@ public class VehicleUnitXmlExtractionService {
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(), vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
path path
)); ));
vuCardIwIntervals.add(new VuCardIwInterval( addVuCardIwInterval(vuCardIwIntervals, new VuCardIwInterval(
driverKey, driverKey,
normalizeToken(text(record, "cardSlotNumber")), normalizeVuSlot(text(record, "cardSlotNumber")),
from, from,
to, to,
path path
@ -270,7 +270,7 @@ public class VehicleUnitXmlExtractionService {
parsedChanges.add(new ActivityChange( parsedChanges.add(new ActivityChange(
from, from,
normalizeActivity(text(change, "activity")), normalizeActivity(text(change, "activity")),
normalizeToken(text(change, "slot")), normalizeVuSlot(text(change, "slot")),
normalizeToken(text(change, "cardStatus")), normalizeToken(text(change, "cardStatus")),
normalizeToken(text(change, "drivingStatus")), normalizeToken(text(change, "drivingStatus")),
dayPath + "/activityChangeInfos[" + (changeIndex + 1) + "]" dayPath + "/activityChangeInfos[" + (changeIndex + 1) + "]"
@ -967,11 +967,12 @@ public class VehicleUnitXmlExtractionService {
String explicitSlot, String explicitSlot,
List<VuCardIwInterval> vuCardIwIntervals List<VuCardIwInterval> vuCardIwIntervals
) { ) {
String normalizedExplicitSlot = normalizeVuSlot(explicitSlot);
if (explicitDriverKey != null) { if (explicitDriverKey != null) {
return List.of(new DriverAssignment(explicitDriverKey, explicitSlot)); return List.of(new DriverAssignment(explicitDriverKey, normalizedExplicitSlot));
} }
return vuCardIwIntervals.stream() return vuCardIwIntervals.stream()
.filter(iw -> explicitSlot == null || explicitSlot.equals(iw.slot())) .filter(iw -> normalizedExplicitSlot == null || normalizedExplicitSlot.equals(iw.slot()))
.filter(iw -> iw.covers(occurredAt)) .filter(iw -> iw.covers(occurredAt))
.map(iw -> new DriverAssignment(iw.driverKey(), iw.slot())) .map(iw -> new DriverAssignment(iw.driverKey(), iw.slot()))
.toList(); .toList();
@ -985,6 +986,22 @@ public class VehicleUnitXmlExtractionService {
return List.copyOf(unique.values()); return List.copyOf(unique.values());
} }
private void addVuCardIwInterval(List<VuCardIwInterval> intervals, VuCardIwInterval candidate) {
boolean alreadyPresent = intervals.stream().anyMatch(existing ->
sameVuCardIwInterval(existing, candidate)
);
if (!alreadyPresent) {
intervals.add(candidate);
}
}
private boolean sameVuCardIwInterval(VuCardIwInterval existing, VuCardIwInterval candidate) {
return java.util.Objects.equals(existing.driverKey(), candidate.driverKey())
&& java.util.Objects.equals(existing.slot(), candidate.slot())
&& java.util.Objects.equals(existing.from(), candidate.from())
&& java.util.Objects.equals(existing.to(), candidate.to());
}
private String driverKeyFromCardNode(Element node, String basePath) { private String driverKeyFromCardNode(Element node, String basePath) {
String cardNation = text(node, basePath + "/cardIssuingMemberState"); String cardNation = text(node, basePath + "/cardIssuingMemberState");
String cardNumber = driverKeyFactory.canonicalCardNumber(joinCardNumber(node, basePath + "/cardNumber")); String cardNumber = driverKeyFactory.canonicalCardNumber(joinCardNumber(node, basePath + "/cardNumber"));
@ -1073,6 +1090,18 @@ public class VehicleUnitXmlExtractionService {
return value.trim().toUpperCase().replace('-', '_').replace(' ', '_'); return value.trim().toUpperCase().replace('-', '_').replace(' ', '_');
} }
private String normalizeVuSlot(String value) {
String normalized = normalizeToken(value);
if (normalized == null) {
return null;
}
return switch (normalized) {
case "0", "DRIVER" -> "DRIVER";
case "1", "CO_DRIVER", "CODRIVER" -> "CO_DRIVER";
default -> normalized;
};
}
private String mapPlaceEntryType(String entryType) { private String mapPlaceEntryType(String entryType) {
String normalized = normalizeToken(entryType); String normalized = normalizeToken(entryType);
if (normalized == null) { if (normalized == null) {
@ -1200,6 +1229,27 @@ public class VehicleUnitXmlExtractionService {
} }
} }
private void addVehicleUsageInterval(ExtractedCardVehicleUsageInterval candidate) {
boolean alreadyPresent = vehicleUsageIntervals.stream().anyMatch(existing ->
sameVehicleUsageInterval(existing, candidate)
);
if (!alreadyPresent) {
vehicleUsageIntervals.add(candidate);
}
}
private boolean sameVehicleUsageInterval(
ExtractedCardVehicleUsageInterval existing,
ExtractedCardVehicleUsageInterval candidate
) {
return java.util.Objects.equals(existing.from(), candidate.from())
&& java.util.Objects.equals(existing.to(), candidate.to())
&& java.util.Objects.equals(existing.odometerBeginKm(), candidate.odometerBeginKm())
&& java.util.Objects.equals(existing.odometerEndKm(), candidate.odometerEndKm())
&& java.util.Objects.equals(existing.registrationKey(), candidate.registrationKey())
&& java.util.Objects.equals(existing.vehicleKey(), candidate.vehicleKey());
}
private DriverExtractionSession build() { private DriverExtractionSession build() {
return new DriverExtractionSession( return new DriverExtractionSession(
driverKey, driverKey,

View File

@ -140,6 +140,60 @@ class VehicleUnitXmlExtractionServiceTest {
assertThat(session.warnings()).isEmpty(); assertThat(session.warnings()).isEmpty();
} }
@Test
void matchesNumericVuCardSlotNumbersToDriverActivitySlots() throws Exception {
TachographFileSession session = service.extract(
new TachographXmlParser.ParsedTachographXml(document(VehicleUnitXmlSamples.vehicleUnitXmlWithNumericCardSlots()), "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()).extracting("slot").containsOnly("DRIVER");
assertThat(session.warnings()).isEmpty();
}
@Test
void deduplicatesEquivalentVehicleUnitVehicleUsageIntervals() throws Exception {
TachographFileSession session = service.extract(
new TachographXmlParser.ParsedTachographXml(document(VehicleUnitXmlSamples.vehicleUnitXmlWithDuplicateVehicleUsageInterval()), "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 firstDriver = session.driversByKey().get("12:12345678901200");
assertThat(firstDriver).isNotNull();
assertThat(firstDriver.cardVehicleUsageIntervals()).hasSize(1);
assertThat(firstDriver.cardVehicleUsageIntervals().get(0).from()).isEqualTo(OffsetDateTime.parse("2026-04-01T08:00:00Z"));
assertThat(firstDriver.cardVehicleUsageIntervals().get(0).to()).isEqualTo(OffsetDateTime.parse("2026-04-01T11:00:00Z"));
}
@Test @Test
void keepsOpenVuCardUsageRecordWithoutClosingItAtDownloadPeriodEnd() throws Exception { void keepsOpenVuCardUsageRecordWithoutClosingItAtDownloadPeriodEnd() throws Exception {
TachographFileSession session = service.extract( TachographFileSession session = service.extract(

View File

@ -314,4 +314,41 @@ final class VehicleUnitXmlSamples {
</VehicleUnit> </VehicleUnit>
"""; """;
} }
static String vehicleUnitXmlWithNumericCardSlots() {
return vehicleUnitXmlWithActivityDownloadTime()
.replace("<cardSlotNumber>DRIVER</cardSlotNumber>", "<cardSlotNumber>0</cardSlotNumber>");
}
static String vehicleUnitXmlWithDuplicateVehicleUsageInterval() {
String duplicateRecord = """
<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>2</generation>
</fullCardNumber>
<cardExpiryDate>2031-04-01T00:00:00Z</cardExpiryDate>
<cardInsertionTime>2026-04-01T08:00:00Z</cardInsertionTime>
<vehicleOdometerValueAtInsertion>1000</vehicleOdometerValueAtInsertion>
<cardSlotNumber>DRIVER</cardSlotNumber>
<cardWithdrawalTime>2026-04-01T11:00:00Z</cardWithdrawalTime>
<vehicleOdometerValueAtWithdrawal>1100</vehicleOdometerValueAtWithdrawal>
<manualInputFlag>NO_ENTRY</manualInputFlag>
</vuCardIWRecords>
""";
return vehicleUnitXml().replace(
duplicateRecord,
duplicateRecord + duplicateRecord
);
}
} }