Add vehicle unit tachograph file sessions
This commit is contained in:
parent
39714b90b3
commit
6b43a4b0e8
|
|
@ -248,6 +248,49 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Create tachograph vehicle unit file session",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "formdata",
|
||||||
|
"formdata": [
|
||||||
|
{
|
||||||
|
"key": "file",
|
||||||
|
"type": "file",
|
||||||
|
"src": "{{tachographVuDddFile}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "tenantKey",
|
||||||
|
"type": "text",
|
||||||
|
"value": "{{tenantKey}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "sourceInstanceKey",
|
||||||
|
"type": "text",
|
||||||
|
"value": "legalrequirements-vehicleunit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "sessionLabel",
|
||||||
|
"type": "text",
|
||||||
|
"value": "vehicle-unit upload sample"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/eventhub/tachograph-file-sessions",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"eventhub",
|
||||||
|
"tachograph-file-sessions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Get tachograph file session",
|
"name": "Get tachograph file session",
|
||||||
"request": {
|
"request": {
|
||||||
|
|
@ -374,6 +417,10 @@
|
||||||
{
|
{
|
||||||
"key": "tachographDddFile",
|
"key": "tachographDddFile",
|
||||||
"value": "C:\\\\temp\\\\driver-card.ddd"
|
"value": "C:\\\\temp\\\\driver-card.ddd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "tachographVuDddFile",
|
||||||
|
"value": "C:\\\\temp\\\\vehicle-unit.ddd"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ public class LegalRequirementsClient {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LegalRequirementsUploadResult uploadDriverCard(byte[] fileBytes, String fileName) {
|
public LegalRequirementsUploadResult uploadTachographFile(byte[] fileBytes, String fileName) {
|
||||||
EventHubProperties.LegalRequirements config = properties.getTachographFileSession().getLegalRequirements();
|
EventHubProperties.LegalRequirements config = properties.getTachographFileSession().getLegalRequirements();
|
||||||
HttpClient client = HttpClient.newBuilder()
|
HttpClient client = HttpClient.newBuilder()
|
||||||
.connectTimeout(config.getConnectTimeout())
|
.connectTimeout(config.getConnectTimeout())
|
||||||
|
|
|
||||||
|
|
@ -28,20 +28,23 @@ public class TachographFileSessionService {
|
||||||
private final TachographFileSessionRepository repository;
|
private final TachographFileSessionRepository repository;
|
||||||
private final LegalRequirementsClient legalRequirementsClient;
|
private final LegalRequirementsClient legalRequirementsClient;
|
||||||
private final TachographXmlParser tachographXmlParser;
|
private final TachographXmlParser tachographXmlParser;
|
||||||
private final DriverCardXmlExtractionService extractionService;
|
private final DriverCardXmlExtractionService driverCardExtractionService;
|
||||||
|
private final VehicleUnitXmlExtractionService vehicleUnitExtractionService;
|
||||||
|
|
||||||
public TachographFileSessionService(
|
public TachographFileSessionService(
|
||||||
EventHubProperties properties,
|
EventHubProperties properties,
|
||||||
TachographFileSessionRepository repository,
|
TachographFileSessionRepository repository,
|
||||||
LegalRequirementsClient legalRequirementsClient,
|
LegalRequirementsClient legalRequirementsClient,
|
||||||
TachographXmlParser tachographXmlParser,
|
TachographXmlParser tachographXmlParser,
|
||||||
DriverCardXmlExtractionService extractionService
|
DriverCardXmlExtractionService driverCardExtractionService,
|
||||||
|
VehicleUnitXmlExtractionService vehicleUnitExtractionService
|
||||||
) {
|
) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.legalRequirementsClient = legalRequirementsClient;
|
this.legalRequirementsClient = legalRequirementsClient;
|
||||||
this.tachographXmlParser = tachographXmlParser;
|
this.tachographXmlParser = tachographXmlParser;
|
||||||
this.extractionService = extractionService;
|
this.driverCardExtractionService = driverCardExtractionService;
|
||||||
|
this.vehicleUnitExtractionService = vehicleUnitExtractionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreateTachographFileSessionResponse createSession(
|
public CreateTachographFileSessionResponse createSession(
|
||||||
|
|
@ -54,23 +57,26 @@ public class TachographFileSessionService {
|
||||||
validateFile(file);
|
validateFile(file);
|
||||||
byte[] fileBytes = file.getBytes();
|
byte[] fileBytes = file.getBytes();
|
||||||
validateFileBytes(fileBytes);
|
validateFileBytes(fileBytes);
|
||||||
LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadDriverCard(fileBytes, file.getOriginalFilename());
|
LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadTachographFile(fileBytes, file.getOriginalFilename());
|
||||||
TachographXmlParser.ParsedTachographXml parsedXml = tachographXmlParser.parseDriverCardXml(uploadResult.xmlContent());
|
TachographXmlParser.ParsedTachographXml parsedXml = tachographXmlParser.parse(uploadResult.xmlContent());
|
||||||
Instant createdAt = Instant.now();
|
Instant createdAt = Instant.now();
|
||||||
Instant expiresAt = createdAt.plus(properties.getTachographFileSession().getTtl());
|
Instant expiresAt = createdAt.plus(properties.getTachographFileSession().getTtl());
|
||||||
|
boolean driverCardFile = "DriverCard".equals(parsedXml.rootElementName());
|
||||||
TachographFileSessionMetadata metadata = new TachographFileSessionMetadata(
|
TachographFileSessionMetadata metadata = new TachographFileSessionMetadata(
|
||||||
tenant(tenantKey),
|
tenant(tenantKey),
|
||||||
sourceInstance(sourceInstanceKey),
|
sourceInstance(sourceInstanceKey, driverCardFile),
|
||||||
blankToNull(sessionLabel),
|
blankToNull(sessionLabel),
|
||||||
file.getOriginalFilename(),
|
file.getOriginalFilename(),
|
||||||
sha256(fileBytes),
|
sha256(fileBytes),
|
||||||
fileBytes.length,
|
fileBytes.length,
|
||||||
uploadResult.dataPackageId(),
|
uploadResult.dataPackageId(),
|
||||||
sha256(uploadResult.xmlContent().getBytes(StandardCharsets.UTF_8)),
|
sha256(uploadResult.xmlContent().getBytes(StandardCharsets.UTF_8)),
|
||||||
true,
|
driverCardFile,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
TachographFileSession session = extractionService.extract(parsedXml, metadata, createdAt, expiresAt);
|
TachographFileSession session = driverCardFile
|
||||||
|
? driverCardExtractionService.extract(parsedXml, metadata, createdAt, expiresAt)
|
||||||
|
: vehicleUnitExtractionService.extract(parsedXml, metadata, createdAt, expiresAt);
|
||||||
TachographFileSession saved = repository.save(session);
|
TachographFileSession saved = repository.save(session);
|
||||||
return new CreateTachographFileSessionResponse(toSummary(saved));
|
return new CreateTachographFileSessionResponse(toSummary(saved));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
@ -171,8 +177,11 @@ public class TachographFileSessionService {
|
||||||
return blankToNull(tenantKey) == null ? "default" : tenantKey.trim();
|
return blankToNull(tenantKey) == null ? "default" : tenantKey.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String sourceInstance(String sourceInstanceKey) {
|
private String sourceInstance(String sourceInstanceKey, boolean driverCardFile) {
|
||||||
return blankToNull(sourceInstanceKey) == null ? "legalrequirements-drivercard" : sourceInstanceKey.trim();
|
if (blankToNull(sourceInstanceKey) != null) {
|
||||||
|
return sourceInstanceKey.trim();
|
||||||
|
}
|
||||||
|
return driverCardFile ? "legalrequirements-drivercard" : "legalrequirements-vehicleunit";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String blankToNull(String value) {
|
private String blankToNull(String value) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ public class TachographXmlParser {
|
||||||
this.schema = loadSchema();
|
this.schema = loadSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParsedTachographXml parseDriverCardXml(String xmlContent) {
|
public ParsedTachographXml parse(String xmlContent) {
|
||||||
try {
|
try {
|
||||||
validate(xmlContent);
|
validate(xmlContent);
|
||||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
|
@ -44,8 +44,8 @@ public class TachographXmlParser {
|
||||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
Document document = builder.parse(new InputSource(new StringReader(xmlContent)));
|
Document document = builder.parse(new InputSource(new StringReader(xmlContent)));
|
||||||
String rootName = document.getDocumentElement() == null ? null : document.getDocumentElement().getNodeName();
|
String rootName = document.getDocumentElement() == null ? null : document.getDocumentElement().getNodeName();
|
||||||
if (!"DriverCard".equals(rootName)) {
|
if (!"DriverCard".equals(rootName) && !"VehicleUnit".equals(rootName)) {
|
||||||
throw new UnsupportedTachographFileTypeException("Only DriverCard XML documents are supported in phase 1.");
|
throw new UnsupportedTachographFileTypeException("Only DriverCard and VehicleUnit XML documents are supported.");
|
||||||
}
|
}
|
||||||
return new ParsedTachographXml(document, rootName);
|
return new ParsedTachographXml(document, rootName);
|
||||||
} catch (ParserConfigurationException | IOException | SAXException e) {
|
} catch (ParserConfigurationException | IOException | SAXException e) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedDriver;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicle;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.xml.xpath.XPath;
|
||||||
|
import javax.xml.xpath.XPathConstants;
|
||||||
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
import javax.xml.xpath.XPathFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class VehicleUnitXmlExtractionService {
|
||||||
|
|
||||||
|
private final DriverKeyFactory driverKeyFactory;
|
||||||
|
private final VehicleKeyFactory vehicleKeyFactory;
|
||||||
|
|
||||||
|
public VehicleUnitXmlExtractionService(DriverKeyFactory driverKeyFactory, VehicleKeyFactory vehicleKeyFactory) {
|
||||||
|
this.driverKeyFactory = driverKeyFactory;
|
||||||
|
this.vehicleKeyFactory = vehicleKeyFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TachographFileSession extract(
|
||||||
|
TachographXmlParser.ParsedTachographXml parsedXml,
|
||||||
|
TachographFileSessionMetadata metadata,
|
||||||
|
Instant createdAt,
|
||||||
|
Instant expiresAt
|
||||||
|
) {
|
||||||
|
Document document = parsedXml.document();
|
||||||
|
List<ExtractionWarning> sessionWarnings = new ArrayList<>();
|
||||||
|
|
||||||
|
VehicleContext vehicleContext = extractVehicleContext(document, sessionWarnings);
|
||||||
|
Map<String, DriverExtractionBuilder> driversByKey = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
if (nodes(document, "/VehicleUnit/Activities/vuActivityDailyData").getLength() > 0) {
|
||||||
|
sessionWarnings.add(new ExtractionWarning(
|
||||||
|
"VU_ACTIVITY_NOT_EXTRACTED",
|
||||||
|
"Vehicle-unit daily activity data is present but is not yet mapped into card activity intervals in this phase.",
|
||||||
|
"/VehicleUnit/Activities/vuActivityDailyData"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeList records = nodes(document, "/VehicleUnit/Activities/vuCardIWData/vuCardIWRecords");
|
||||||
|
for (int i = 0; i < records.getLength(); i++) {
|
||||||
|
Element record = (Element) records.item(i);
|
||||||
|
String path = "/VehicleUnit/Activities/vuCardIWData/vuCardIWRecords[" + (i + 1) + "]";
|
||||||
|
String driverIdentification = text(record, "fullCardNumber/cardNumber/driverIdentification");
|
||||||
|
if (driverIdentification == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String cardNation = text(record, "fullCardNumber/cardIssuingMemberState");
|
||||||
|
String cardNumber = joinCardNumber(record, "fullCardNumber/cardNumber");
|
||||||
|
if (cardNumber == null) {
|
||||||
|
sessionWarnings.add(new ExtractionWarning(
|
||||||
|
"MISSING_VU_DRIVER_CARD",
|
||||||
|
"Vehicle-unit insertion/withdrawal record is missing a usable driver card number.",
|
||||||
|
path
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String driverKey = driverKeyFactory.createDriverKey(cardNation, cardNumber);
|
||||||
|
DriverExtractionBuilder builder = driversByKey.computeIfAbsent(
|
||||||
|
driverKey,
|
||||||
|
ignored -> new DriverExtractionBuilder(driverKey, driverKeyFactory.createSourceDriverId(driverKey))
|
||||||
|
);
|
||||||
|
builder.mergeDriver(
|
||||||
|
text(record, "cardHolderName/holderSurname/name"),
|
||||||
|
text(record, "cardHolderName/holderFirstNames/name")
|
||||||
|
);
|
||||||
|
builder.mergeDriverCard(
|
||||||
|
driverKeyFactory.createSourceDriverCardId(driverKey),
|
||||||
|
cardNation,
|
||||||
|
cardNumber,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
offsetDateTime(text(record, "cardExpiryDate"))
|
||||||
|
);
|
||||||
|
builder.addVehicleContext(vehicleContext.registration(), vehicleContext.vehicle());
|
||||||
|
|
||||||
|
OffsetDateTime from = offsetDateTime(text(record, "cardInsertionTime"));
|
||||||
|
if (from == null) {
|
||||||
|
sessionWarnings.add(new ExtractionWarning(
|
||||||
|
"MISSING_VU_CARD_INSERTION",
|
||||||
|
"Vehicle-unit insertion/withdrawal record is missing cardInsertionTime.",
|
||||||
|
path
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
OffsetDateTime to = offsetDateTime(text(record, "cardWithdrawalTime"));
|
||||||
|
if (to == null) {
|
||||||
|
to = vehicleContext.defaultOpenIntervalEnd();
|
||||||
|
sessionWarnings.add(new ExtractionWarning(
|
||||||
|
"OPEN_VU_CARD_INTERVAL",
|
||||||
|
"Vehicle-unit insertion/withdrawal record has no withdrawal time; interval was closed using the VU downloadable-period end.",
|
||||||
|
path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (to == null || to.isBefore(from)) {
|
||||||
|
sessionWarnings.add(new ExtractionWarning(
|
||||||
|
"INVALID_VU_CARD_INTERVAL",
|
||||||
|
"Vehicle-unit insertion/withdrawal record has an invalid interval range.",
|
||||||
|
path
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.vehicleUsageIntervals.add(new ExtractedCardVehicleUsageInterval(
|
||||||
|
"VUIW-" + (i + 1),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
longValue(text(record, "vehicleOdometerValueAtInsertion")),
|
||||||
|
longValue(text(record, "vehicleOdometerValueAtWithdrawal")),
|
||||||
|
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||||
|
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||||
|
path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (driversByKey.isEmpty()) {
|
||||||
|
sessionWarnings.add(new ExtractionWarning(
|
||||||
|
"NO_DRIVER_CARD_IW_DATA",
|
||||||
|
"Vehicle-unit XML did not contain driver-card insertion/withdrawal records to build driver sessions.",
|
||||||
|
"/VehicleUnit/Activities"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ExtractionWarning> allWarnings = new ArrayList<>(sessionWarnings);
|
||||||
|
Map<String, DriverExtractionSession> driverSessions = new LinkedHashMap<>();
|
||||||
|
int activityCount = 0;
|
||||||
|
int vehicleUsageCount = 0;
|
||||||
|
for (DriverExtractionBuilder builder : driversByKey.values()) {
|
||||||
|
builder.vehicleUsageIntervals.sort(Comparator.comparing(ExtractedCardVehicleUsageInterval::from));
|
||||||
|
allWarnings.addAll(builder.warnings);
|
||||||
|
activityCount += builder.cardActivityIntervals.size();
|
||||||
|
vehicleUsageCount += builder.vehicleUsageIntervals.size();
|
||||||
|
driverSessions.put(builder.driverKey, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
int registrationCount = vehicleContext.registration() == null ? 0 : 1;
|
||||||
|
int vehicleCount = vehicleContext.vehicle() == null ? 0 : 1;
|
||||||
|
ExtractionStats stats = new ExtractionStats(
|
||||||
|
driverSessions.size(),
|
||||||
|
activityCount,
|
||||||
|
vehicleUsageCount,
|
||||||
|
registrationCount,
|
||||||
|
vehicleCount,
|
||||||
|
allWarnings.size()
|
||||||
|
);
|
||||||
|
|
||||||
|
return new TachographFileSession(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
metadata,
|
||||||
|
Map.copyOf(driverSessions),
|
||||||
|
stats,
|
||||||
|
List.copyOf(allWarnings),
|
||||||
|
createdAt,
|
||||||
|
expiresAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VehicleContext extractVehicleContext(Document document, List<ExtractionWarning> warnings) {
|
||||||
|
Element overview = firstElement(document, "/VehicleUnit/Overview[1]");
|
||||||
|
if (overview == null) {
|
||||||
|
warnings.add(new ExtractionWarning(
|
||||||
|
"MISSING_VU_OVERVIEW",
|
||||||
|
"Vehicle-unit XML does not contain an Overview block.",
|
||||||
|
"/VehicleUnit"
|
||||||
|
));
|
||||||
|
return new VehicleContext(null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
String registrationNation = text(overview, "vehicleRegistrationIdentification/vehicleRegistrationNation");
|
||||||
|
String registrationNumber = text(overview, "vehicleRegistrationIdentification/vehicleRegistrationNumber/vehicleRegNumber");
|
||||||
|
ExtractedVehicleRegistration registration = null;
|
||||||
|
String registrationKey = registrationNation == null && registrationNumber == null
|
||||||
|
? null
|
||||||
|
: vehicleKeyFactory.createRegistrationKey(registrationNation, registrationNumber);
|
||||||
|
if (registrationKey != null) {
|
||||||
|
registration = new ExtractedVehicleRegistration(
|
||||||
|
registrationKey,
|
||||||
|
vehicleKeyFactory.createSourceVehicleRegistrationId(registrationKey),
|
||||||
|
registrationNation,
|
||||||
|
registrationNumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String vin = text(overview, "vehicleIdentificationNumber");
|
||||||
|
ExtractedVehicle vehicle = null;
|
||||||
|
String vehicleKey = vehicleKeyFactory.createVehicleKey(vin);
|
||||||
|
if (vehicleKey != null) {
|
||||||
|
vehicle = new ExtractedVehicle(
|
||||||
|
vehicleKey,
|
||||||
|
vehicleKeyFactory.createSourceVehicleId(vehicleKey),
|
||||||
|
vin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OffsetDateTime defaultEnd = offsetDateTime(text(overview, "vuDownloadablePeriod/maxDownloadableTime"));
|
||||||
|
return new VehicleContext(registration, vehicle, defaultEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Element firstElement(Object node, String expression) {
|
||||||
|
NodeList nodes = nodes(node, expression);
|
||||||
|
if (nodes.getLength() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (Element) nodes.item(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeList nodes(Object node, String expression) {
|
||||||
|
try {
|
||||||
|
XPath xpath = XPathFactory.newInstance().newXPath();
|
||||||
|
return (NodeList) xpath.evaluate(expression, node, XPathConstants.NODESET);
|
||||||
|
} catch (XPathExpressionException e) {
|
||||||
|
throw new IllegalStateException("Invalid XPath expression: " + expression, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(Object node, String expression) {
|
||||||
|
try {
|
||||||
|
XPath xpath = XPathFactory.newInstance().newXPath();
|
||||||
|
String value = xpath.evaluate(expression, node);
|
||||||
|
return value == null || value.isBlank() ? null : value.trim();
|
||||||
|
} catch (XPathExpressionException e) {
|
||||||
|
throw new IllegalStateException("Invalid XPath expression: " + expression, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OffsetDateTime offsetDateTime(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return OffsetDateTime.parse(value.trim()).withOffsetSameInstant(ZoneOffset.UTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long longValue(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Long.parseLong(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joinCardNumber(Element node, String basePath) {
|
||||||
|
String driverIdentification = text(node, basePath + "/driverIdentification");
|
||||||
|
if (driverIdentification == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String replacement = text(node, basePath + "/cardReplacementIndex");
|
||||||
|
String renewal = text(node, basePath + "/cardRenewalIndex");
|
||||||
|
StringBuilder builder = new StringBuilder(driverIdentification);
|
||||||
|
if (replacement != null) {
|
||||||
|
builder.append(replacement);
|
||||||
|
}
|
||||||
|
if (renewal != null) {
|
||||||
|
builder.append(renewal);
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DriverExtractionBuilder {
|
||||||
|
private final String driverKey;
|
||||||
|
private final String sourceDriverId;
|
||||||
|
private ExtractedDriver driver;
|
||||||
|
private ExtractedDriverCard driverCard;
|
||||||
|
private final Map<String, ExtractedVehicleRegistration> registrationsByKey = new LinkedHashMap<>();
|
||||||
|
private final Map<String, ExtractedVehicle> vehiclesByKey = new LinkedHashMap<>();
|
||||||
|
private final List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals = new ArrayList<>();
|
||||||
|
private final List<ExtractedCardActivityInterval> cardActivityIntervals = new ArrayList<>();
|
||||||
|
private final List<ExtractionWarning> warnings = new ArrayList<>();
|
||||||
|
|
||||||
|
private DriverExtractionBuilder(String driverKey, String sourceDriverId) {
|
||||||
|
this.driverKey = driverKey;
|
||||||
|
this.sourceDriverId = sourceDriverId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeDriver(String surname, String firstNames) {
|
||||||
|
if (driver == null) {
|
||||||
|
driver = new ExtractedDriver(
|
||||||
|
driverKey,
|
||||||
|
sourceDriverId,
|
||||||
|
surname,
|
||||||
|
firstNames,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeDriverCard(
|
||||||
|
String sourceDriverCardId,
|
||||||
|
String cardNation,
|
||||||
|
String cardNumber,
|
||||||
|
String issuingAuthorityName,
|
||||||
|
OffsetDateTime issueDate,
|
||||||
|
OffsetDateTime validityBegin,
|
||||||
|
OffsetDateTime expiryDate
|
||||||
|
) {
|
||||||
|
if (driverCard == null) {
|
||||||
|
driverCard = new ExtractedDriverCard(
|
||||||
|
sourceDriverCardId,
|
||||||
|
cardNation,
|
||||||
|
cardNumber,
|
||||||
|
issuingAuthorityName,
|
||||||
|
issueDate,
|
||||||
|
validityBegin,
|
||||||
|
expiryDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addVehicleContext(ExtractedVehicleRegistration registration, ExtractedVehicle vehicle) {
|
||||||
|
if (registration != null) {
|
||||||
|
registrationsByKey.putIfAbsent(registration.registrationKey(), registration);
|
||||||
|
}
|
||||||
|
if (vehicle != null) {
|
||||||
|
vehiclesByKey.putIfAbsent(vehicle.vehicleKey(), vehicle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DriverExtractionSession build() {
|
||||||
|
return new DriverExtractionSession(
|
||||||
|
driverKey,
|
||||||
|
driver,
|
||||||
|
driverCard,
|
||||||
|
List.copyOf(registrationsByKey.values()),
|
||||||
|
List.copyOf(vehiclesByKey.values()),
|
||||||
|
List.copyOf(vehicleUsageIntervals),
|
||||||
|
List.copyOf(cardActivityIntervals),
|
||||||
|
List.copyOf(warnings)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record VehicleContext(
|
||||||
|
ExtractedVehicleRegistration registration,
|
||||||
|
ExtractedVehicle vehicle,
|
||||||
|
OffsetDateTime defaultOpenIntervalEnd
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,7 @@ class DriverCardXmlExtractionServiceTest {
|
||||||
@Test
|
@Test
|
||||||
void extractsDriverCardVehiclesAndActivities() {
|
void extractsDriverCardVehiclesAndActivities() {
|
||||||
TachographFileSession session = service.extract(
|
TachographFileSession session = service.extract(
|
||||||
parser.parseDriverCardXml(DriverCardXmlSamples.validDriverCardXml()),
|
parser.parse(DriverCardXmlSamples.validDriverCardXml()),
|
||||||
new TachographFileSessionMetadata(
|
new TachographFileSessionMetadata(
|
||||||
"default",
|
"default",
|
||||||
"legalrequirements-drivercard",
|
"legalrequirements-drivercard",
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,22 @@ class TachographFileSessionServiceTest {
|
||||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||||
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||||
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||||
DriverCardXmlExtractionService extractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
DriverCardXmlExtractionService driverCardExtractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||||
TachographFileSessionService service = new TachographFileSessionService(properties, repository, client, parser, extractor);
|
VehicleUnitXmlExtractionService vehicleUnitExtractor = Mockito.mock(VehicleUnitXmlExtractionService.class);
|
||||||
|
TachographFileSessionService service = new TachographFileSessionService(
|
||||||
|
properties,
|
||||||
|
repository,
|
||||||
|
client,
|
||||||
|
parser,
|
||||||
|
driverCardExtractor,
|
||||||
|
vehicleUnitExtractor
|
||||||
|
);
|
||||||
|
|
||||||
MockMultipartFile file = new MockMultipartFile("file", "sample.ddd", "application/octet-stream", "abc".getBytes(StandardCharsets.UTF_8));
|
MockMultipartFile file = new MockMultipartFile("file", "sample.ddd", "application/octet-stream", "abc".getBytes(StandardCharsets.UTF_8));
|
||||||
when(client.uploadDriverCard(any(), eq("sample.ddd"))).thenReturn(new LegalRequirementsUploadResult("42", "<DriverCard/>"));
|
when(client.uploadTachographFile(any(), eq("sample.ddd"))).thenReturn(new LegalRequirementsUploadResult("42", "<DriverCard/>"));
|
||||||
TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class);
|
TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class);
|
||||||
when(parser.parseDriverCardXml("<DriverCard/>")).thenReturn(parsed);
|
when(parsed.rootElementName()).thenReturn("DriverCard");
|
||||||
|
when(parser.parse("<DriverCard/>")).thenReturn(parsed);
|
||||||
TachographFileSession extracted = new TachographFileSession(
|
TachographFileSession extracted = new TachographFileSession(
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null),
|
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null),
|
||||||
|
|
@ -44,7 +53,7 @@ class TachographFileSessionServiceTest {
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
Instant.now().plusSeconds(10)
|
Instant.now().plusSeconds(10)
|
||||||
);
|
);
|
||||||
when(extractor.extract(eq(parsed), any(), any(), any())).thenReturn(extracted);
|
when(driverCardExtractor.extract(eq(parsed), any(), any(), any())).thenReturn(extracted);
|
||||||
|
|
||||||
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "sample");
|
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "sample");
|
||||||
|
|
||||||
|
|
@ -59,8 +68,16 @@ class TachographFileSessionServiceTest {
|
||||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||||
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||||
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||||
DriverCardXmlExtractionService extractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
DriverCardXmlExtractionService driverCardExtractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||||
TachographFileSessionService service = new TachographFileSessionService(properties, repository, client, parser, extractor);
|
VehicleUnitXmlExtractionService vehicleUnitExtractor = Mockito.mock(VehicleUnitXmlExtractionService.class);
|
||||||
|
TachographFileSessionService service = new TachographFileSessionService(
|
||||||
|
properties,
|
||||||
|
repository,
|
||||||
|
client,
|
||||||
|
parser,
|
||||||
|
driverCardExtractor,
|
||||||
|
vehicleUnitExtractor
|
||||||
|
);
|
||||||
|
|
||||||
MockMultipartFile file = new MockMultipartFile("file", "sample.ddd", "application/octet-stream", new byte[1025]);
|
MockMultipartFile file = new MockMultipartFile("file", "sample.ddd", "application/octet-stream", new byte[1025]);
|
||||||
|
|
||||||
|
|
@ -68,6 +85,45 @@ class TachographFileSessionServiceTest {
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessageContaining("size limit");
|
.hasMessageContaining("size limit");
|
||||||
|
|
||||||
verifyNoInteractions(client, parser, extractor);
|
verifyNoInteractions(client, parser, driverCardExtractor, vehicleUnitExtractor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void usesVehicleUnitDefaultsAndExtractorForVehicleUnitXml() {
|
||||||
|
EventHubProperties properties = new EventHubProperties();
|
||||||
|
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||||
|
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||||
|
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||||
|
DriverCardXmlExtractionService driverCardExtractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||||
|
VehicleUnitXmlExtractionService vehicleUnitExtractor = Mockito.mock(VehicleUnitXmlExtractionService.class);
|
||||||
|
TachographFileSessionService service = new TachographFileSessionService(
|
||||||
|
properties,
|
||||||
|
repository,
|
||||||
|
client,
|
||||||
|
parser,
|
||||||
|
driverCardExtractor,
|
||||||
|
vehicleUnitExtractor
|
||||||
|
);
|
||||||
|
|
||||||
|
MockMultipartFile file = new MockMultipartFile("file", "vu.ddd", "application/octet-stream", "vu".getBytes(StandardCharsets.UTF_8));
|
||||||
|
when(client.uploadTachographFile(any(), eq("vu.ddd"))).thenReturn(new LegalRequirementsUploadResult("77", "<VehicleUnit/>"));
|
||||||
|
TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class);
|
||||||
|
when(parsed.rootElementName()).thenReturn("VehicleUnit");
|
||||||
|
when(parser.parse("<VehicleUnit/>")).thenReturn(parsed);
|
||||||
|
TachographFileSession extracted = new TachographFileSession(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
new TachographFileSessionMetadata("default", "legalrequirements-vehicleunit", "vu", "vu.ddd", "a", 2, "77", "b", false, null),
|
||||||
|
Map.of(),
|
||||||
|
new ExtractionStats(0, 0, 0, 0, 0, 0),
|
||||||
|
java.util.List.of(),
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now().plusSeconds(10)
|
||||||
|
);
|
||||||
|
when(vehicleUnitExtractor.extract(eq(parsed), any(), any(), any())).thenReturn(extracted);
|
||||||
|
|
||||||
|
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "vu");
|
||||||
|
|
||||||
|
assertThat(response.session().driverCardFile()).isFalse();
|
||||||
|
assertThat(response.session().sourceInstanceKey()).isEqualTo("legalrequirements-vehicleunit");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,25 @@ class TachographXmlParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void parsesValidDriverCardXml() {
|
void parsesValidDriverCardXml() {
|
||||||
TachographXmlParser.ParsedTachographXml parsed = parser.parseDriverCardXml(DriverCardXmlSamples.validDriverCardXml());
|
TachographXmlParser.ParsedTachographXml parsed = parser.parse(DriverCardXmlSamples.validDriverCardXml());
|
||||||
|
|
||||||
assertThat(parsed.rootElementName()).isEqualTo("DriverCard");
|
assertThat(parsed.rootElementName()).isEqualTo("DriverCard");
|
||||||
assertThat(parsed.document().getDocumentElement().getNodeName()).isEqualTo("DriverCard");
|
assertThat(parsed.document().getDocumentElement().getNodeName()).isEqualTo("DriverCard");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesValidVehicleUnitXml() {
|
||||||
|
TachographXmlParser.ParsedTachographXml parsed = parser.parse("<VehicleUnit/>");
|
||||||
|
|
||||||
|
assertThat(parsed.rootElementName()).isEqualTo("VehicleUnit");
|
||||||
|
assertThat(parsed.document().getDocumentElement().getNodeName()).isEqualTo("VehicleUnit");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsInvalidXmlAgainstSchema() {
|
void rejectsInvalidXmlAgainstSchema() {
|
||||||
String invalid = "<DriverCard><Identification></DriverCard>";
|
String invalid = "<DriverCard><Identification></DriverCard>";
|
||||||
|
|
||||||
assertThatThrownBy(() -> parser.parseDriverCardXml(invalid))
|
assertThatThrownBy(() -> parser.parse(invalid))
|
||||||
.isInstanceOf(TachographXmlValidationException.class);
|
.isInstanceOf(TachographXmlValidationException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +42,7 @@ class TachographXmlParserTest {
|
||||||
<DriverCard/>
|
<DriverCard/>
|
||||||
""";
|
""";
|
||||||
|
|
||||||
assertThatThrownBy(() -> parser.parseDriverCardXml(xmlWithDoctype))
|
assertThatThrownBy(() -> parser.parse(xmlWithDoctype))
|
||||||
.isInstanceOf(TachographXmlValidationException.class);
|
.isInstanceOf(TachographXmlValidationException.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
|
|
||||||
|
class VehicleUnitXmlExtractionServiceTest {
|
||||||
|
|
||||||
|
private final VehicleUnitXmlExtractionService service = new VehicleUnitXmlExtractionService(
|
||||||
|
new DriverKeyFactory(),
|
||||||
|
new VehicleKeyFactory()
|
||||||
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractsVehicleUnitDriverSessionsFromCardInsertionWithdrawalData() throws Exception {
|
||||||
|
TachographFileSession session = service.extract(
|
||||||
|
new TachographXmlParser.ParsedTachographXml(document(VehicleUnitXmlSamples.vehicleUnitXml()), "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)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(session.driversByKey()).hasSize(2);
|
||||||
|
DriverExtractionSession firstDriver = session.driversByKey().get("12:12345678901200");
|
||||||
|
DriverExtractionSession secondDriver = session.driversByKey().get("12:99999999999911");
|
||||||
|
assertThat(firstDriver).isNotNull();
|
||||||
|
assertThat(secondDriver).isNotNull();
|
||||||
|
|
||||||
|
assertThat(firstDriver.driver().surname()).isEqualTo("Muster");
|
||||||
|
assertThat(firstDriver.vehicleRegistrations()).extracting("registrationKey").containsExactly("12:W-1000V");
|
||||||
|
assertThat(firstDriver.vehicles()).extracting("vehicleKey").containsExactly("VINVU123456789012");
|
||||||
|
assertThat(firstDriver.cardVehicleUsageIntervals()).hasSize(1);
|
||||||
|
assertThat(firstDriver.cardVehicleUsageIntervals().get(0).from().toString()).isEqualTo("2026-04-01T08:00Z");
|
||||||
|
assertThat(firstDriver.cardVehicleUsageIntervals().get(0).to().toString()).isEqualTo("2026-04-01T11:00Z");
|
||||||
|
|
||||||
|
assertThat(secondDriver.cardVehicleUsageIntervals()).hasSize(1);
|
||||||
|
assertThat(secondDriver.cardVehicleUsageIntervals().get(0).to().toString()).isEqualTo("2026-04-02T10:00Z");
|
||||||
|
|
||||||
|
assertThat(session.warnings()).extracting("code")
|
||||||
|
.contains("VU_ACTIVITY_NOT_EXTRACTED", "OPEN_VU_CARD_INTERVAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document document(String xml) throws Exception {
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(false);
|
||||||
|
return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
final class VehicleUnitXmlSamples {
|
||||||
|
|
||||||
|
private VehicleUnitXmlSamples() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static String vehicleUnitXml() {
|
||||||
|
return """
|
||||||
|
<VehicleUnit>
|
||||||
|
<Overview>
|
||||||
|
<vehicleIdentificationNumber>VINVU123456789012</vehicleIdentificationNumber>
|
||||||
|
<vehicleRegistrationIdentification>
|
||||||
|
<vehicleRegistrationNation>12</vehicleRegistrationNation>
|
||||||
|
<vehicleRegistrationNumber><vehicleRegNumber>W-1000V</vehicleRegNumber></vehicleRegistrationNumber>
|
||||||
|
</vehicleRegistrationIdentification>
|
||||||
|
<vuDownloadablePeriod>
|
||||||
|
<maxDownloadableTime>2026-04-02T10:00:00Z</maxDownloadableTime>
|
||||||
|
</vuDownloadablePeriod>
|
||||||
|
</Overview>
|
||||||
|
<Activities>
|
||||||
|
<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>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>
|
||||||
|
<vuCardIWRecords>
|
||||||
|
<cardHolderName>
|
||||||
|
<holderSurname><name>Test</name></holderSurname>
|
||||||
|
<holderFirstNames><name>Tina</name></holderFirstNames>
|
||||||
|
</cardHolderName>
|
||||||
|
<fullCardNumber>
|
||||||
|
<cardType>DRIVER_CARD</cardType>
|
||||||
|
<cardIssuingMemberState>12</cardIssuingMemberState>
|
||||||
|
<cardNumber>
|
||||||
|
<driverIdentification>999999999999</driverIdentification>
|
||||||
|
<cardReplacementIndex>1</cardReplacementIndex>
|
||||||
|
<cardRenewalIndex>1</cardRenewalIndex>
|
||||||
|
</cardNumber>
|
||||||
|
<generation>2</generation>
|
||||||
|
</fullCardNumber>
|
||||||
|
<cardInsertionTime>2026-04-02T07:30:00Z</cardInsertionTime>
|
||||||
|
<vehicleOdometerValueAtInsertion>1200</vehicleOdometerValueAtInsertion>
|
||||||
|
<cardSlotNumber>DRIVER</cardSlotNumber>
|
||||||
|
<manualInputFlag>MANUAL_ENTRIES</manualInputFlag>
|
||||||
|
</vuCardIWRecords>
|
||||||
|
</vuCardIWData>
|
||||||
|
<vuActivityDailyData/>
|
||||||
|
</Activities>
|
||||||
|
</VehicleUnit>
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue