Fix VU interval deduplication and slot normalization
This commit is contained in:
parent
ce81bfe868
commit
d7ecf52330
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue