From 997983b3ea2dce3d18233c1d8c0c1910944c96e8 Mon Sep 17 00:00:00 2001
From: Jan Zickermann <jan.zickermann@dataport.de>
Date: Mon, 14 Oct 2024 10:37:56 +0200
Subject: [PATCH] OZG-6748 KOP-2734 Implement fetchMessage

---
 .../xta/client/FetchMessageParameter.java     |  36 +
 .../client/FetchMessageParameterFactory.java  |  16 +
 .../de/ozgcloud/xta/client/XtaClient.java     | 195 +++---
 .../ozgcloud/xta/client/XtaClientFactory.java |  10 +-
 .../xta/client/config/XtaClientConfig.java    |   5 +
 .../xta/client/core/XtaClientService.java     | 142 ++++
 .../client/core/XtaClientServiceFactory.java  |  26 +
 .../exception/ClientRuntimeException.java     |  12 +
 .../FetchMessageParameterFactoryTest.java     |  13 +
 .../xta/client/FetchMessageParameterTest.java | 108 +++
 .../xta/client/XtaClientFactoryTest.java      |  10 +-
 .../ozgcloud/xta/client/XtaClientITCase.java  | 119 +---
 .../xta/client/XtaClientRemoteITCase.java     | 132 +---
 .../de/ozgcloud/xta/client/XtaClientTest.java | 641 +++++++++++++++---
 .../xta/client/core/XtaClientServiceTest.java | 427 ++++++++++++
 .../XtaServerSetupExtensionTestUtil.java      |  41 +-
 .../XtaMessageMetaDataListingTestFactory.java |  31 +
 .../XtaTransportReportTestFactory.java        |  21 +
 18 files changed, 1527 insertions(+), 458 deletions(-)
 create mode 100644 src/main/java/de/ozgcloud/xta/client/FetchMessageParameter.java
 create mode 100644 src/main/java/de/ozgcloud/xta/client/FetchMessageParameterFactory.java
 create mode 100644 src/main/java/de/ozgcloud/xta/client/core/XtaClientService.java
 create mode 100644 src/main/java/de/ozgcloud/xta/client/core/XtaClientServiceFactory.java
 create mode 100644 src/main/java/de/ozgcloud/xta/client/exception/ClientRuntimeException.java
 create mode 100644 src/test/java/de/ozgcloud/xta/client/FetchMessageParameterFactoryTest.java
 create mode 100644 src/test/java/de/ozgcloud/xta/client/FetchMessageParameterTest.java
 create mode 100644 src/test/java/de/ozgcloud/xta/client/core/XtaClientServiceTest.java
 create mode 100644 src/test/java/de/ozgcloud/xta/client/factory/XtaMessageMetaDataListingTestFactory.java
 create mode 100644 src/test/java/de/ozgcloud/xta/client/factory/XtaTransportReportTestFactory.java

diff --git a/src/main/java/de/ozgcloud/xta/client/FetchMessageParameter.java b/src/main/java/de/ozgcloud/xta/client/FetchMessageParameter.java
new file mode 100644
index 0000000..b2ae07f
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/FetchMessageParameter.java
@@ -0,0 +1,36 @@
+package de.ozgcloud.xta.client;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import de.ozgcloud.xta.client.model.XtaIdentifier;
+import de.ozgcloud.xta.client.model.XtaMessage;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
+
+record FetchMessageParameter(
+		XtaIdentifier clientIdentifier,
+		Consumer<XtaMessage> processMessage,
+		Set<String> viewedMessageIds
+) {
+
+	public FetchMessageParameter withViewedMessageIdsFrom(List<XtaMessageMetaData> messageMetaData) {
+		return new FetchMessageParameter(this.clientIdentifier, this.processMessage,
+				collectSetOfViewedMessageIds(messageMetaData, this.viewedMessageIds));
+	}
+
+	private Set<String> collectSetOfViewedMessageIds(List<XtaMessageMetaData> messageMetaData, Set<String> processedMessageIds) {
+		return Stream.concat(
+				processedMessageIds.stream(),
+				messageMetaData.stream()
+						.map(XtaMessageMetaData::messageId)
+		).collect(Collectors.toSet());
+	}
+
+	public boolean hasMessageAlreadyBeenViewed(XtaMessageMetaData messageMetaData) {
+		return viewedMessageIds.contains(messageMetaData.messageId());
+	}
+
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/FetchMessageParameterFactory.java b/src/main/java/de/ozgcloud/xta/client/FetchMessageParameterFactory.java
new file mode 100644
index 0000000..34f3297
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/FetchMessageParameterFactory.java
@@ -0,0 +1,16 @@
+package de.ozgcloud.xta.client;
+
+import java.util.Collections;
+import java.util.function.Consumer;
+
+import de.ozgcloud.xta.client.model.XtaIdentifier;
+import de.ozgcloud.xta.client.model.XtaMessage;
+import lombok.Builder;
+
+@Builder
+class FetchMessageParameterFactory {
+
+	public FetchMessageParameter create(XtaIdentifier clientIdentifier, Consumer<XtaMessage> processMessage) {
+		return new FetchMessageParameter(clientIdentifier,  processMessage, Collections.emptySet());
+	}
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/XtaClient.java b/src/main/java/de/ozgcloud/xta/client/XtaClient.java
index a22c002..b487ce9 100644
--- a/src/main/java/de/ozgcloud/xta/client/XtaClient.java
+++ b/src/main/java/de/ozgcloud/xta/client/XtaClient.java
@@ -1,113 +1,142 @@
 package de.ozgcloud.xta.client;
 
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
 import jakarta.validation.Valid;
-import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
-import de.ozgcloud.xta.client.core.WrappedXtaService;
+import de.ozgcloud.xta.client.core.XtaClientService;
+import de.ozgcloud.xta.client.exception.ClientRuntimeException;
 import de.ozgcloud.xta.client.model.XtaIdentifier;
 import de.ozgcloud.xta.client.model.XtaMessage;
-import de.ozgcloud.xta.client.model.XtaMessageAndTransportReport;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
 import de.ozgcloud.xta.client.model.XtaMessageMetaDataListing;
+import de.ozgcloud.xta.client.model.XtaMessageStatus;
 import de.ozgcloud.xta.client.model.XtaTransportReport;
-import genv3.de.xoev.transport.xta.x211.InvalidMessageIDException;
-import genv3.de.xoev.transport.xta.x211.MessageSchemaViolationException;
-import genv3.de.xoev.transport.xta.x211.MessageVirusDetectionException;
-import genv3.de.xoev.transport.xta.x211.ParameterIsNotValidException;
-import genv3.de.xoev.transport.xta.x211.PermissionDeniedException;
-import genv3.de.xoev.transport.xta.x211.SyncAsyncException;
-import genv3.de.xoev.transport.xta.x211.XTAWSTechnicalProblemException;
 import lombok.AccessLevel;
 import lombok.Builder;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 
 @RequiredArgsConstructor(access = AccessLevel.MODULE)
 @Builder(access = AccessLevel.MODULE)
 @Getter(AccessLevel.MODULE)
+@Slf4j
 public class XtaClient {
 
-	private final WrappedXtaService service;
+	private final XtaClientService service;
 	private final XtaClientConfig config;
+	private final FetchMessageParameterFactory fetchMessageParameterFactory;
 
-	/**
-	 * Fetch metadata of pending messages sent to the {@code xtaIdentifier}. The returned listing contains at most
-	 * {@link de.ozgcloud.xta.client.config.XtaClientConfig#getMaxListItems() maxListItems} messages. To fetch the next messages, use
-	 * {@link #getNextMessagesMetadata(String)}. Note that {@code xtaIdentifier} has to be configured as a
-	 * {@link de.ozgcloud.xta.client.config.XtaClientConfig#getClientIdentifiers() clientIdentifiers}.
-	 *
-	 * @param clientIdentifier the client identifier value to fetch messages for
-	 * @return the listing result with metadata of messages
-	 */
-	public XtaMessageMetaDataListing getMessagesMetadata(@NotBlank String clientIdentifier)
-			throws XTAWSTechnicalProblemException, PermissionDeniedException {
-		var identifier = deriveIdentifier(clientIdentifier);
-		service.checkAccountActive(identifier);
-		return getStatusList(identifier);
-	}
-
-	/**
-	 * Fetch metadata of pending messages sent to the {@code xtaIdentifier}. This method skips checks but otherwise behaves exactly as
-	 * {@link #getMessagesMetadata(String)}.
-	 */
-	public XtaMessageMetaDataListing getNextMessagesMetadata(@NotBlank String clientIdentifier)
-			throws XTAWSTechnicalProblemException, PermissionDeniedException {
-		return getStatusList(deriveIdentifier(clientIdentifier));
-	}
-
-	private XtaMessageMetaDataListing getStatusList(XtaIdentifier clientIdentifier) throws XTAWSTechnicalProblemException, PermissionDeniedException {
-		return service.getStatusList(clientIdentifier, config.getMaxListItems());
-	}
-
-	/**
-	 * Fetch the message content, close the message, and then fetch the transport report for the given {@code messageId} and reader identifier
-	 * {@code clientIdentifier}.
-	 *
-	 * @param clientIdentifier Identifier of the reading client
-	 * @param messageId        Identifier of the message to fetch
-	 * @return The message and transport report
-	 */
-	public XtaMessageAndTransportReport getMessage(@NotBlank String clientIdentifier, @NotBlank String messageId)
-			throws XTAWSTechnicalProblemException, PermissionDeniedException, InvalidMessageIDException {
-		var identifier = deriveIdentifier(clientIdentifier);
-
-		var message = service.getMessage(messageId, identifier);
-		service.close(messageId, identifier);
-
-		var transportReport = service.getTransportReport(messageId, identifier);
-		return XtaMessageAndTransportReport.builder()
-				.message(message)
-				.transportReport(transportReport)
-				.build();
-	}
-
-	public XtaTransportReport sendMessage(@Valid XtaMessage messageWithoutMessageId)
-			throws XTAWSTechnicalProblemException, PermissionDeniedException, InvalidMessageIDException, SyncAsyncException,
-			MessageVirusDetectionException, MessageSchemaViolationException, ParameterIsNotValidException {
+	public List<XtaTransportReport> fetchMessages(@NotNull Consumer<XtaMessage> processMessage) {
+		return config.getClientIdentifiers().stream()
+				.filter(service::checkAccountActive)
+				.map(clientIdentifier -> fetchMessageParameterFactory.create(clientIdentifier, processMessage))
+				.flatMap(this::fetchMessagesForClientIdentifier)
+				.toList();
+	}
 
-		var metaData = messageWithoutMessageId.metaData();
-		var authorIdentifier = metaData.authorIdentifier();
-		service.checkAccountActive(authorIdentifier);
-		service.lookupService(metaData.service(), metaData.readerIdentifier(), authorIdentifier);
+	Stream<XtaTransportReport> fetchMessagesForClientIdentifier(FetchMessageParameter parameter) {
+		return service.getStatusList(parameter.clientIdentifier())
+				.map(listing -> {
+					var transportReports = fetchMessagesForListing(listing.messages(), parameter);
+					return Stream.concat(transportReports.stream(),
+							checkMoreMessagesAvailable(listing, transportReports)
+									? fetchMessagesForClientIdentifier(parameter.withViewedMessageIdsFrom(listing.messages()))
+									: Stream.empty());
+				})
+				.orElse(Stream.empty());
+	}
 
-		var messageId = service.createMessageId(authorIdentifier);
-		service.sendMessage(createXtaMessageWithMessageId(messageWithoutMessageId, messageId));
+	boolean checkMoreMessagesAvailable(XtaMessageMetaDataListing listing, List<XtaTransportReport> transportReports) {
+		return checkExtraPendingMessagesAvailable(listing) && checkSomeMessageHasBeenClosed(transportReports);
+	}
 
-		return service.getTransportReport(messageId, authorIdentifier);
+	boolean checkExtraPendingMessagesAvailable(XtaMessageMetaDataListing listing) {
+		return listing.messages().size() < listing.pendingMessageCount().intValue();
 	}
 
-	XtaMessage createXtaMessageWithMessageId(XtaMessage messageWithoutMessageId, String messageId) {
-		return messageWithoutMessageId.toBuilder()
-				.metaData(messageWithoutMessageId.metaData().toBuilder()
-						.messageId(messageId)
-						.build())
-				.build();
+	boolean checkSomeMessageHasBeenClosed(List<XtaTransportReport> transportReports) {
+		var someTransportReportHasGreenStatus = transportReports.stream()
+				.anyMatch(t -> t.status().equals(XtaMessageStatus.GREEN));
+		if (!someTransportReportHasGreenStatus) {
+			logWarnForNoMessageClosed();
+		}
+		return someTransportReportHasGreenStatus;
 	}
 
-	XtaIdentifier deriveIdentifier(String xtaIdentifier) {
-		return config.getClientIdentifiers().stream()
-				.filter(id -> id.value().equals(xtaIdentifier))
-				.findFirst()
-				.orElseThrow(() -> new IllegalArgumentException("Unknown identifier: " + xtaIdentifier));
+	void logWarnForNoMessageClosed() {
+		log.warn("No message has been closed although more are pending. Try increasing max list items.");
+	}
+
+	List<XtaTransportReport> fetchMessagesForListing(
+			List<XtaMessageMetaData> messageMetaDataItems,
+			FetchMessageParameter parameter
+	) {
+		return messageMetaDataItems.stream()
+				.filter(metaData -> checkMessageShouldBeFetched(metaData, parameter))
+				.map(metaData -> fetchMessage(metaData, parameter))
+				.flatMap(Optional::stream)
+				.toList();
+	}
+
+	boolean checkMessageShouldBeFetched(XtaMessageMetaData messageMetaData, FetchMessageParameter parameter) {
+		return !parameter.hasMessageAlreadyBeenViewed(messageMetaData) && isMessageSupported(messageMetaData);
+	}
+
+	boolean isMessageSupported(XtaMessageMetaData messageMetaData) {
+		return Optional.ofNullable(config.getIsMessageSupported())
+				.map(predicate -> predicate.test(messageMetaData))
+				.orElse(true);
+	}
+
+	Optional<XtaTransportReport> fetchMessage(XtaMessageMetaData messageMetaData, FetchMessageParameter parameter) {
+		return service.getMessage(messageMetaData)
+				.flatMap(message -> processMesssageAndFetchTransportReport(message, parameter));
 	}
+
+	Optional<XtaTransportReport> processMesssageAndFetchTransportReport(XtaMessage message, FetchMessageParameter parameter) {
+		processMessageAndCloseIfNoException(message, parameter.processMessage());
+		return service.getTransportReport(message.metaData());
+	}
+
+	void processMessageAndCloseIfNoException(XtaMessage message, Consumer<XtaMessage> processMessage) {
+		var messageId = message.metaData().messageId();
+		try {
+			processMessage.accept(message);
+			log.debug("Processing of message '{}' succeeded! Closing message.", messageId);
+			service.closeMessage(message);
+		} catch (RuntimeException exception) {
+			logErrorForMessageProcessingFailure(messageId, exception);
+		}
+	}
+
+	void logErrorForMessageProcessingFailure(String messageId, RuntimeException exception) {
+		log.error("Processing of message '%s' failed! Not closing message.".formatted(messageId), exception);
+	}
+
+	public XtaTransportReport sendMessage(@Valid XtaMessage messageWithoutMessageId) {
+		var metaData = messageWithoutMessageId.metaData();
+		throwExceptionIfAccountInactive(metaData.authorIdentifier());
+		throwExceptionIfServiceNotAvailable(metaData);
+		return service.sendMessage(messageWithoutMessageId);
+	}
+
+	void throwExceptionIfServiceNotAvailable(XtaMessageMetaData metaData) {
+		if (!service.lookupService(metaData)) {
+			throw new ClientRuntimeException("Service %s is not available!".formatted(metaData.service()));
+		}
+	}
+
+	void throwExceptionIfAccountInactive(XtaIdentifier clientIdentifier) {
+		if (!service.checkAccountActive(clientIdentifier)) {
+			throw new ClientRuntimeException("Account %s is not active!".formatted(clientIdentifier.value()));
+		}
+	}
+
 }
diff --git a/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java b/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java
index 8d9c3ec..4cc93a3 100644
--- a/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java
+++ b/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java
@@ -2,7 +2,7 @@ package de.ozgcloud.xta.client;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
 import de.ozgcloud.xta.client.config.XtaConfigValidator;
-import de.ozgcloud.xta.client.core.WrappedXtaServiceFactory;
+import de.ozgcloud.xta.client.core.XtaClientServiceFactory;
 import de.ozgcloud.xta.client.exception.ClientInitializationException;
 import lombok.Builder;
 import lombok.RequiredArgsConstructor;
@@ -12,14 +12,16 @@ import lombok.RequiredArgsConstructor;
 public class XtaClientFactory {
 
 	private final XtaConfigValidator configValidator;
-	private final WrappedXtaServiceFactory wrappedXtaServiceFactory;
+	private final XtaClientServiceFactory xtaClientServiceFactory;
 	private final XtaClientConfig config;
+	private final FetchMessageParameterFactory fetchMessageParameterFactory;
 
 	public static XtaClientFactory from(final XtaClientConfig config) {
 		return XtaClientFactory.builder()
 				.config(config)
 				.configValidator(XtaConfigValidator.builder().build())
-				.wrappedXtaServiceFactory(WrappedXtaServiceFactory.from(config))
+				.xtaClientServiceFactory(XtaClientServiceFactory.from(config))
+				.fetchMessageParameterFactory(FetchMessageParameterFactory.builder().build())
 				.build();
 	}
 
@@ -27,7 +29,7 @@ public class XtaClientFactory {
 		configValidator.validate(config);
 		return XtaClient.builder()
 				.config(config)
-				.service(wrappedXtaServiceFactory.create())
+				.service(xtaClientServiceFactory.create())
 				.build();
 	}
 }
diff --git a/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java b/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
index 184ce5b..b2b8603 100644
--- a/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
+++ b/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
@@ -12,6 +12,7 @@
 package de.ozgcloud.xta.client.config;
 
 import java.util.List;
+import java.util.function.Predicate;
 
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotBlank;
@@ -20,6 +21,7 @@ import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Positive;
 
 import de.ozgcloud.xta.client.model.XtaIdentifier;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
 import lombok.Builder;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
@@ -34,6 +36,9 @@ public class XtaClientConfig {
 	@NotEmpty(message = "at least one client identifier is required")
 	private final List<@Valid XtaIdentifier> clientIdentifiers;
 
+	@Builder.Default
+	private final Predicate<XtaMessageMetaData> isMessageSupported = null;
+
 	@NotBlank
 	private final String managementServiceUrl;
 	@NotBlank
diff --git a/src/main/java/de/ozgcloud/xta/client/core/XtaClientService.java b/src/main/java/de/ozgcloud/xta/client/core/XtaClientService.java
new file mode 100644
index 0000000..3c1aacc
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/core/XtaClientService.java
@@ -0,0 +1,142 @@
+package de.ozgcloud.xta.client.core;
+
+import java.util.Optional;
+
+import de.ozgcloud.xta.client.config.XtaClientConfig;
+import de.ozgcloud.xta.client.exception.ClientRuntimeException;
+import de.ozgcloud.xta.client.model.XtaIdentifier;
+import de.ozgcloud.xta.client.model.XtaMessage;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
+import de.ozgcloud.xta.client.model.XtaMessageMetaDataListing;
+import de.ozgcloud.xta.client.model.XtaTransportReport;
+import genv3.de.xoev.transport.xta.x211.InvalidMessageIDException;
+import genv3.de.xoev.transport.xta.x211.MessageSchemaViolationException;
+import genv3.de.xoev.transport.xta.x211.MessageVirusDetectionException;
+import genv3.de.xoev.transport.xta.x211.ParameterIsNotValidException;
+import genv3.de.xoev.transport.xta.x211.PermissionDeniedException;
+import genv3.de.xoev.transport.xta.x211.SyncAsyncException;
+import genv3.de.xoev.transport.xta.x211.XTAWSTechnicalProblemException;
+import lombok.Builder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Builder
+@RequiredArgsConstructor
+@Slf4j
+public class XtaClientService {
+
+	private final WrappedXtaService service;
+	private final XtaClientConfig config;
+
+	public Optional<XtaTransportReport> getTransportReport(XtaMessageMetaData messageMetaData) {
+		try {
+			return Optional.of(getTransportReportOrThrowException(messageMetaData));
+		} catch (ClientRuntimeException e) {
+			logError("Failed to get transport report!", e);
+			return Optional.empty();
+		}
+	}
+
+	public void closeMessage(XtaMessage message) {
+		var messageId = message.metaData().messageId();
+		var readerClientIdentifier = message.metaData().readerIdentifier();
+		try {
+			service.close(messageId, readerClientIdentifier);
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | InvalidMessageIDException e) {
+			logError("Failed to close message! '%s' (reader: %s)".formatted(messageId, readerClientIdentifier), e);
+		}
+	}
+
+	public Optional<XtaMessage> getMessage(XtaMessageMetaData messageMetaData) {
+		var messageId = messageMetaData.messageId();
+		var readerClientIdentifier = messageMetaData.readerIdentifier();
+		try {
+			return Optional.of(service.getMessage(messageId, readerClientIdentifier));
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | InvalidMessageIDException e) {
+			logError("Failed to get message by id ! '%s' (reader: %s)".formatted(messageId, readerClientIdentifier.value()), e);
+			return Optional.empty();
+		}
+	}
+
+	public Optional<XtaMessageMetaDataListing> getStatusList(XtaIdentifier clientIdentifier) {
+		try {
+			return Optional.of(service.getStatusList(clientIdentifier, config.getMaxListItems()));
+		} catch (PermissionDeniedException | XTAWSTechnicalProblemException e) {
+			logError("Failed to get status list!", e);
+			return Optional.empty();
+		}
+	}
+
+	void logError(String message, Exception e) {
+		log.error(message, e);
+	}
+
+	public boolean checkAccountActive(XtaIdentifier clientIdentifier) {
+		try {
+			service.checkAccountActive(clientIdentifier);
+			return true;
+		} catch (XTAWSTechnicalProblemException e) {
+			throw new ClientRuntimeException("Failed to check account active!", e);
+		} catch (PermissionDeniedException e) {
+			return false;
+		}
+	}
+
+	public XtaTransportReport sendMessage(XtaMessage messageWithoutMessageId) {
+		var message = getXtaMessageWithMessageId(messageWithoutMessageId);
+		try {
+			service.sendMessage(message);
+			return getTransportReportOrThrowException(message.metaData());
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | SyncAsyncException | ParameterIsNotValidException |
+				MessageVirusDetectionException | MessageSchemaViolationException e) {
+			throw new ClientRuntimeException("Failed to send message!", e);
+		}
+	}
+
+	XtaMessage getXtaMessageWithMessageId(XtaMessage messageWithoutMessageId) {
+		var authorIdentifier = messageWithoutMessageId.metaData().authorIdentifier();
+		try {
+			var messageId = service.createMessageId(authorIdentifier);
+			return createXtaMessageWithMessageId(messageWithoutMessageId, messageId);
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException e) {
+			throw new ClientRuntimeException("Failed to create message id!", e);
+		}
+	}
+
+	XtaMessage createXtaMessageWithMessageId(XtaMessage messageWithoutMessageId, String messageId) {
+		return messageWithoutMessageId.toBuilder()
+				.metaData(messageWithoutMessageId.metaData().toBuilder()
+						.messageId(messageId)
+						.build())
+				.build();
+	}
+
+	XtaTransportReport getTransportReportOrThrowException(XtaMessageMetaData messageMetaData) {
+		var messageId = messageMetaData.messageId();
+		var authorId = messageMetaData.authorIdentifier();
+		try {
+			return service.getTransportReport(messageId, authorId);
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | InvalidMessageIDException e) {
+			throw new ClientRuntimeException(
+					"Failed to get transport report! (messageId: %s, reader: %s)".formatted(messageId, authorId.value()), e);
+		}
+	}
+
+	public boolean lookupService(XtaMessageMetaData messageMetaData) {
+		try {
+			return service.lookupService(
+					messageMetaData.service(),
+					messageMetaData.readerIdentifier(),
+					messageMetaData.authorIdentifier()
+			);
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | ParameterIsNotValidException exception) {
+			logWarning("Failed to lookup service for message '%s'!".formatted(messageMetaData.messageId()), exception);
+			return false;
+		}
+	}
+
+	void logWarning(String message, Exception exception) {
+		log.warn(message, exception);
+	}
+
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/core/XtaClientServiceFactory.java b/src/main/java/de/ozgcloud/xta/client/core/XtaClientServiceFactory.java
new file mode 100644
index 0000000..61a95de
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/core/XtaClientServiceFactory.java
@@ -0,0 +1,26 @@
+package de.ozgcloud.xta.client.core;
+
+import de.ozgcloud.xta.client.config.XtaClientConfig;
+import de.ozgcloud.xta.client.exception.ClientInitializationException;
+import lombok.Builder;
+
+@Builder
+public class XtaClientServiceFactory {
+
+	private final WrappedXtaServiceFactory wrappedXtaServiceFactory;
+	private final XtaClientConfig config;
+
+	public static XtaClientServiceFactory from(XtaClientConfig config) {
+		return XtaClientServiceFactory.builder()
+				.config(config)
+				.wrappedXtaServiceFactory(WrappedXtaServiceFactory.from(config))
+				.build();
+	}
+
+	public XtaClientService create() throws ClientInitializationException {
+		return XtaClientService.builder()
+				.config(config)
+				.service(wrappedXtaServiceFactory.create())
+				.build();
+	}
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/exception/ClientRuntimeException.java b/src/main/java/de/ozgcloud/xta/client/exception/ClientRuntimeException.java
new file mode 100644
index 0000000..d2e32e5
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/exception/ClientRuntimeException.java
@@ -0,0 +1,12 @@
+package de.ozgcloud.xta.client.exception;
+
+public class ClientRuntimeException extends RuntimeException {
+
+	public ClientRuntimeException(String message) {
+		super(message);
+	}
+
+	public ClientRuntimeException(String message, Throwable cause) {
+		super(message, cause);
+	}
+}
diff --git a/src/test/java/de/ozgcloud/xta/client/FetchMessageParameterFactoryTest.java b/src/test/java/de/ozgcloud/xta/client/FetchMessageParameterFactoryTest.java
new file mode 100644
index 0000000..b7984c3
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/FetchMessageParameterFactoryTest.java
@@ -0,0 +1,13 @@
+package de.ozgcloud.xta.client;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+
+class FetchMessageParameterFactoryTest {
+
+	@DisplayName("create")
+	@Nested
+	class TestCreate {
+
+	}
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/xta/client/FetchMessageParameterTest.java b/src/test/java/de/ozgcloud/xta/client/FetchMessageParameterTest.java
new file mode 100644
index 0000000..4ae4671
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/FetchMessageParameterTest.java
@@ -0,0 +1,108 @@
+package de.ozgcloud.xta.client;
+
+import static de.ozgcloud.xta.client.factory.MessageMetaDataTestFactory.*;
+import static de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory.*;
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import de.ozgcloud.xta.client.factory.XtaMessageMetaDataTestFactory;
+import de.ozgcloud.xta.client.model.XtaMessage;
+
+class FetchMessageParameterTest {
+
+	private Consumer<XtaMessage> processMessage;
+
+	private FetchMessageParameter parameter;
+
+	@BeforeEach
+	void setup() {
+		processMessage = message -> {
+		};
+		parameter = new FetchMessageParameter(SELF_IDENTIFIER, processMessage, Set.of(MESSAGE_ID2));
+
+	}
+
+	@DisplayName("with viewed message ids from")
+	@Nested
+	class TestWithViewedMessageIdsFrom {
+
+		@DisplayName("should keep client identifier")
+		@Test
+		void shouldKeepClientIdentifier() {
+			var result = parameter.withViewedMessageIdsFrom(Collections.emptyList());
+
+			assertThat(result.clientIdentifier()).isEqualTo(SELF_IDENTIFIER);
+		}
+
+		@DisplayName("should keep process message")
+		@Test
+		void shouldKeepProcessMessage() {
+			var result = parameter.withViewedMessageIdsFrom(Collections.emptyList());
+
+			assertThat(result.processMessage()).isEqualTo(processMessage);
+		}
+
+		@DisplayName("should return copy")
+		@Test
+		void shouldReturnCopy() {
+			var result = parameter.withViewedMessageIdsFrom(Collections.emptyList());
+
+			assertThat(result.viewedMessageIds()).containsExactly(MESSAGE_ID2);
+		}
+
+		@DisplayName("should return copy with additional viewed message ids")
+		@Test
+		void shouldReturnCopyWithAdditionalViewedMessageIds() {
+			var result = parameter.withViewedMessageIdsFrom(List.of(XtaMessageMetaDataTestFactory.create()));
+
+			assertThat(result.viewedMessageIds()).containsExactlyInAnyOrder(MESSAGE_ID2, MESSAGE_ID);
+		}
+
+		@DisplayName("should not change original with additional viewed message ids")
+		@Test
+		void shouldNotChangeOriginalWithAdditionalViewedMessageIds() {
+			parameter.withViewedMessageIdsFrom(List.of(XtaMessageMetaDataTestFactory.create()));
+
+			assertThat(parameter.viewedMessageIds()).containsExactly(MESSAGE_ID2);
+		}
+	}
+
+	@DisplayName("has message already been viewed")
+	@Nested
+	class TestHasMessageAlreadyBeenViewed {
+
+		@DisplayName("should return true if message id is in viewed message ids")
+		@Test
+		void shouldReturnTrueIfMessageIdIsInViewedMessageIds() {
+			var messageMetaData = XtaMessageMetaDataTestFactory.createBuilder()
+					.messageId(MESSAGE_ID2)
+					.build();
+
+			var result = parameter.hasMessageAlreadyBeenViewed(messageMetaData);
+
+			assertThat(result).isTrue();
+		}
+
+		@DisplayName("should return false if message id is not in viewed message ids")
+		@Test
+		void shouldReturnFalseIfMessageIdIsNotInViewedMessageIds() {
+			var messageMetaData = XtaMessageMetaDataTestFactory.createBuilder()
+					.messageId(MESSAGE_ID3)
+					.build();
+
+			var result = parameter.hasMessageAlreadyBeenViewed(messageMetaData);
+
+			assertThat(result).isFalse();
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java b/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java
index 3077166..a5fecab 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java
@@ -12,8 +12,8 @@ import org.mockito.Mock;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
 import de.ozgcloud.xta.client.config.XtaConfigValidator;
-import de.ozgcloud.xta.client.core.WrappedXtaService;
-import de.ozgcloud.xta.client.core.WrappedXtaServiceFactory;
+import de.ozgcloud.xta.client.core.XtaClientService;
+import de.ozgcloud.xta.client.core.XtaClientServiceFactory;
 import lombok.SneakyThrows;
 
 class XtaClientFactoryTest {
@@ -21,7 +21,7 @@ class XtaClientFactoryTest {
 	@Mock
 	private XtaConfigValidator configValidator;
 	@Mock
-	private WrappedXtaServiceFactory wrappedXtaServiceFactory;
+	private XtaClientServiceFactory xtaClientServiceFactory;
 	@Mock
 	private XtaClientConfig config;
 
@@ -33,12 +33,12 @@ class XtaClientFactoryTest {
 	class TestCreate {
 
 		@Mock
-		private WrappedXtaService service;
+		private XtaClientService service;
 
 		@BeforeEach
 		@SneakyThrows
 		void mock() {
-			when(wrappedXtaServiceFactory.create()).thenReturn(service);
+			when(xtaClientServiceFactory.create()).thenReturn(service);
 		}
 
 		@DisplayName("should have service")
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java b/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
index cf0c0bc..547ab02 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
@@ -10,11 +10,9 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import de.ozgcloud.xta.client.extension.XtaMessageExampleLoader;
-import de.ozgcloud.xta.client.extension.XtaServerSetupExtensionTestUtil;
 import de.ozgcloud.xta.client.extension.XtaTestServerSetupExtension;
 import de.ozgcloud.xta.client.model.XtaMessage;
 import de.ozgcloud.xta.client.model.XtaMessageStatus;
-import genv3.de.xoev.transport.xta.x211.InvalidMessageIDException;
 import lombok.SneakyThrows;
 
 class XtaClientITCase {
@@ -30,121 +28,10 @@ class XtaClientITCase {
 		client = XTA_TEST_SERVER_SETUP_EXTENSION.getClient();
 	}
 
-	@DisplayName("get messages metadata")
+	@DisplayName("fetch messages")
 	@Nested
-	class TestGetMessagesMetadata {
-
-		@DisplayName("with no messages")
-		@Nested
-		class TestWithNoMessages {
-
-			@DisplayName("should return zero pending messages")
-			@Test
-			@SneakyThrows
-			void shouldReturnZeroPendingMessages() {
-				var result = client.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
-
-				assertThat(result.pendingMessageCount()).isZero();
-			}
-		}
-
-		@DisplayName("with one message")
-		@Nested
-		class TestWithOneMessage {
-
-			@BeforeEach
-			void setup() {
-				XTA_TEST_SERVER_SETUP_EXTENSION.sendTestMessage();
-			}
-
-			@DisplayName("should return one pending message for client")
-			@Test
-			@SneakyThrows
-			void shouldReturnOnePendingMessageClient() {
-				var result = client.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
-
-				assertThat(result.pendingMessageCount()).isOne();
-			}
-
-			@DisplayName("should return no pending message for another client")
-			@Test
-			@SneakyThrows
-			void shouldReturnNoPendingMessageForAnotherClient() {
-				var result = client.getMessagesMetadata(READER_CLIENT_IDENTIFIER2.value());
-
-				assertThat(result.pendingMessageCount()).isZero();
-			}
-
-		}
-
-	}
-
-	@DisplayName("get message")
-	@Nested
-	class TestGetMessage {
-
-		private String messageId;
-		private XtaMessage message;
-
-		@BeforeEach
-		@SneakyThrows
-		void setup() {
-			var messageConfig = XtaMessageExampleLoader.MessageExampleConfig.builder()
-					.messageLabel("dfoerdermittel")
-					.reader(READER_CLIENT_IDENTIFIER1)
-					.author(AUTHOR_CLIENT_IDENTIFIER)
-					.build();
-			message = XtaMessageExampleLoader.load(messageConfig);
-			messageId = XtaServerSetupExtensionTestUtil.sendTestMessage(client, message);
-		}
-
-		@DisplayName("should return message with green status")
-		@Test
-		@SneakyThrows
-		void shouldReturnMessageWithGreenStatus() {
-			var result = client.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId);
-
-			assertThat(result.message().metaData().messageId()).isEqualTo(messageId);
-			assertThat(result.transportReport().metaData().messageId()).isEqualTo(messageId);
-			assertThat(result.transportReport().status()).isEqualTo(XtaMessageStatus.GREEN);
-		}
-
-		@DisplayName("should return message with correct message file content")
-		@Test
-		@SneakyThrows
-		void shouldReturnMessageWithCorrectMessageFileContent() {
-			var messageContent = extractMessageFileContent(message);
-
-			var result = client.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId);
-			var resultContent = extractMessageFileContent(result.message());
-
-			assertThat(messageContent).isEqualTo(resultContent);
-		}
-
-		@DisplayName("should not show message id for a closed message in status list")
-		@Test
-		@SneakyThrows
-		void shouldNotShowMessageIdForClosedMessageInStatusList() {
-			assertThatNoException().isThrownBy(() -> client.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId));
-			var metadataResult = client.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
-			if (!metadataResult.messages().isEmpty()) {
-				assertThat(metadataResult.messages()).allMatch(metadata -> !messageId.equals(metadata.messageId()));
-			}
-		}
-
-		@DisplayName("should throw invalid message id exception for modified message id")
-		@Test
-		void shouldThrowInvalidMessageIdExceptionForModifiedMessageId() {
-			assertThatThrownBy(() -> client.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId + "1"))
-					.isInstanceOf(InvalidMessageIDException.class);
-		}
-
-		@DisplayName("should throw invalid message id exception for other client")
-		@Test
-		void shouldThrowInvalidMessageIdExceptionForOtherClient() {
-			assertThatThrownBy(() -> client.getMessage(READER_CLIENT_IDENTIFIER2.value(), messageId))
-					.isInstanceOf(InvalidMessageIDException.class);
-		}
+	class TestFetchMessages {
+		// TODO KOP-2733
 	}
 
 	@DisplayName("send message")
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientRemoteITCase.java b/src/test/java/de/ozgcloud/xta/client/XtaClientRemoteITCase.java
index 42f008a..5aa5fc7 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientRemoteITCase.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientRemoteITCase.java
@@ -3,11 +3,11 @@ package de.ozgcloud.xta.client;
 import static de.ozgcloud.xta.client.extension.XtaServerSetupExtensionTestUtil.*;
 import static org.assertj.core.api.Assertions.*;
 
+import org.junit.Ignore;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -19,8 +19,6 @@ import de.ozgcloud.xta.client.model.XtaFile;
 import de.ozgcloud.xta.client.model.XtaMessage;
 import de.ozgcloud.xta.client.model.XtaMessageStatus;
 import de.ozgcloud.xta.client.xdomea.XdomeaXtaMessageCreatorFactory;
-import genv3.de.xoev.transport.xta.x211.InvalidMessageIDException;
-import genv3.de.xoev.transport.xta.x211.MessageSchemaViolationException;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 
@@ -32,6 +30,7 @@ import lombok.extern.slf4j.Slf4j;
 				"This test requires the path KOP_SH_KIEL_{DEV,TEST}_PATH and password KOP_SH_KIEL_{DEV,TEST}_PASSWORD of KOP_SH_KIEL_DEV.p12 and OZG-CLOUD_SH_KIEL_TEST.pfx. "
 						+ "Additionally, the endpoint of the DEV-xta-server at li33-0005.dp.dsecurecloud.de must be reachable."
 )
+@Ignore
 class XtaClientRemoteITCase {
 
 	@RegisterExtension
@@ -47,121 +46,10 @@ class XtaClientRemoteITCase {
 		readerClient = XTA_REMOTE_SERVER_SETUP_EXTENSION.getReaderClient();
 	}
 
-	@DisplayName("get messages metadata")
+	@DisplayName("fetch messages")
 	@Nested
-	class TestGetMessagesMetadata {
-
-		@DisplayName("with no messages")
-		@Nested
-		class TestWithNoMessages {
-
-			@DisplayName("should return zero pending messages")
-			@Test
-			@SneakyThrows
-			void shouldReturnZeroPendingMessages() {
-				var result = readerClient.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
-
-				assertThat(result.pendingMessageCount()).isZero();
-			}
-		}
-
-		@DisplayName("with one message")
-		@Nested
-		class TestWithOneMessage {
-
-			@BeforeEach
-			void setup() {
-				XTA_REMOTE_SERVER_SETUP_EXTENSION.sendTestMessage();
-			}
-
-			@DisplayName("should return one pending message for client")
-			@Test
-			@SneakyThrows
-			void shouldReturnOnePendingMessageClient() {
-				var result = readerClient.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
-
-				assertThat(result.pendingMessageCount()).isOne();
-			}
-
-			@DisplayName("should return no pending message for another client")
-			@Test
-			@SneakyThrows
-			void shouldReturnNoPendingMessageForAnotherClient() {
-				var result = readerClient.getMessagesMetadata(READER_CLIENT_IDENTIFIER2.value());
-
-				assertThat(result.pendingMessageCount()).isZero();
-			}
-		}
-
-	}
-
-	@DisplayName("get message")
-	@Nested
-	class TestGetMessage {
-
-		private String messageId;
-		private XtaMessage message;
-
-		@BeforeEach
-		@SneakyThrows
-		void setup() {
-			var messageConfig = XtaMessageExampleLoader.MessageExampleConfig.builder()
-					.messageLabel("dfoerdermittel")
-					.reader(READER_CLIENT_IDENTIFIER1)
-					.author(AUTHOR_CLIENT_IDENTIFIER)
-					.build();
-			message = XtaMessageExampleLoader.load(messageConfig);
-			messageId = XTA_REMOTE_SERVER_SETUP_EXTENSION.sendTestMessage(message);
-		}
-
-		@DisplayName("should return message with green status")
-		@Test
-		@SneakyThrows
-		void shouldReturnMessageWithGreenStatus() {
-			var result = readerClient.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId);
-
-			assertThat(result.message().metaData().messageId()).isEqualTo(messageId);
-			assertThat(result.transportReport().metaData().messageId()).isEqualTo(messageId);
-			assertThat(result.transportReport().status()).isEqualTo(XtaMessageStatus.GREEN);
-		}
-
-		@DisplayName("should return message with correct message file content")
-		@Test
-		@SneakyThrows
-		void shouldReturnMessageWithCorrectMessageFileContent() {
-			var messageContent = extractMessageFileContent(message);
-
-			var result = readerClient.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId);
-			var resultContent = extractMessageFileContent(result.message());
-
-			assertThat(messageContent).isEqualTo(resultContent);
-		}
-
-		@DisplayName("should not show message id for a closed message in status list")
-		@Test
-		@SneakyThrows
-		void shouldNotShowMessageIdForClosedMessageInStatusList() {
-			assertThatNoException().isThrownBy(() -> readerClient.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId));
-			var metadataResult = readerClient.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
-			if (!metadataResult.messages().isEmpty()) {
-				assertThat(metadataResult.messages()).allMatch(metadata -> !messageId.equals(metadata.messageId()));
-			}
-		}
-
-		@DisplayName("should throw invalid message id exception for modified message id")
-		@Test
-		void shouldThrowInvalidMessageIdExceptionForModifiedMessageId() {
-			assertThatThrownBy(() -> readerClient.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId + "1"))
-					.isInstanceOf(InvalidMessageIDException.class);
-		}
-
-		@DisplayName("should throw invalid message id exception for other client")
-		@Test
-		void shouldThrowInvalidMessageIdExceptionForOtherClient() {
-			assertThatThrownBy(() -> readerClient.getMessage(READER_CLIENT_IDENTIFIER2.value(), messageId))
-					.isInstanceOf(InvalidMessageIDException.class);
-		}
-
+	class TestFetchMessages {
+		// TODO KOP-2733
 	}
 
 	@DisplayName("send message")
@@ -181,15 +69,9 @@ class XtaClientRemoteITCase {
 		void shouldReturn(String messageLabel) {
 			XtaMessage xtaMessage = createXdomeaMessage(messageLabel);
 
-			try {
-				var result = authorClient.sendMessage(xtaMessage);
-
-				assertThat(result.status()).isEqualTo(XtaMessageStatus.OPEN);
-			} catch (MessageSchemaViolationException exception) {
-				log.error(exception.getFaultInfo().getErrorCode().toString(), exception);
-				fail(exception.getFaultInfo().getErrorCode().toString());
-			}
+			var result = authorClient.sendMessage(xtaMessage);
 
+			assertThat(result.status()).isEqualTo(XtaMessageStatus.OPEN);
 		}
 
 		@SneakyThrows
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientTest.java b/src/test/java/de/ozgcloud/xta/client/XtaClientTest.java
index 76f2e92..37c7ef4 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientTest.java
@@ -5,165 +5,577 @@ import static de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory.*;
 import static org.assertj.core.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
+import java.math.BigInteger;
 import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Spy;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
-import de.ozgcloud.xta.client.core.WrappedXtaService;
+import de.ozgcloud.xta.client.core.XtaClientService;
+import de.ozgcloud.xta.client.exception.ClientRuntimeException;
+import de.ozgcloud.xta.client.factory.XtaMessageMetaDataListingTestFactory;
+import de.ozgcloud.xta.client.factory.XtaMessageMetaDataTestFactory;
 import de.ozgcloud.xta.client.factory.XtaMessageTestFactory;
+import de.ozgcloud.xta.client.factory.XtaTransportReportTestFactory;
 import de.ozgcloud.xta.client.model.XtaMessage;
 import de.ozgcloud.xta.client.model.XtaMessageMetaData;
 import de.ozgcloud.xta.client.model.XtaMessageMetaDataListing;
+import de.ozgcloud.xta.client.model.XtaMessageStatus;
 import de.ozgcloud.xta.client.model.XtaTransportReport;
 import lombok.SneakyThrows;
 
 class XtaClientTest {
 
 	@Mock
-	private WrappedXtaService service;
+	private XtaClientService service;
 
 	@Mock
 	private XtaClientConfig config;
 
+	@Mock
+	private FetchMessageParameterFactory fetchMessageParameterFactory;
+
 	@Spy
 	@InjectMocks
 	private XtaClient client;
 
-	@DisplayName("get messages metadata")
+	private XtaMessageMetaDataListing listing;
+
+	private List<XtaTransportReport> transportReports;
+
+	private XtaMessage message;
+
+	@BeforeEach
+	void setup() {
+		message = XtaMessageTestFactory.create();
+		listing = XtaMessageMetaDataListingTestFactory.create();
+		transportReports = listing.messages().stream()
+				.map(messageMetaData -> XtaTransportReportTestFactory.createBuilder()
+						.metaData(messageMetaData)
+						.build())
+				.toList();
+	}
+
+	@DisplayName("fetch messages")
 	@Nested
-	class TestGetMessagesMetadata {
+	class TestFetchMessages {
+
+		@Mock
+		private Consumer<XtaMessage> processMessage;
 
 		@Mock
-		XtaMessageMetaDataListing xtaMessageMetaDataListing;
+		private FetchMessageParameter parameter;
+
+		@Mock
+		private XtaTransportReport transportReport;
 
 		@BeforeEach
-		@SneakyThrows
 		void mock() {
-			doReturn(SELF_IDENTIFIER).when(client).deriveIdentifier(SELF_IDENTIFIER_VALUE);
-			when(service.getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS)).thenReturn(xtaMessageMetaDataListing);
-			when(config.getMaxListItems()).thenReturn(MAX_LIST_ITEMS);
+			doReturn(List.of(SELF_IDENTIFIER)).when(config).getClientIdentifiers();
 		}
 
-		@DisplayName("should call checkAccountActive")
+		@DisplayName("with active account")
+		@Nested
+		class TestWithActiveAccount {
+
+			@DisplayName("should fetch messages for client identifier")
+			@Test
+			void shouldFetchMessagesForClientIdentifier() {
+				doReturn(true).when(service).checkAccountActive(SELF_IDENTIFIER);
+				when(fetchMessageParameterFactory.create(SELF_IDENTIFIER, processMessage)).thenReturn(parameter);
+				doReturn(Stream.of(transportReport)).when(client).fetchMessagesForClientIdentifier(parameter);
+
+				var result = client.fetchMessages(processMessage);
+
+				assertThat(result).containsExactly(transportReport);
+			}
+		}
+
+		@DisplayName("with inactive account")
+		@Nested
+		class TestWithInactiveAccount {
+
+			@DisplayName("should fetch no messages")
+			@Test
+			void shouldFetchNoMessages() {
+				doReturn(false).when(service).checkAccountActive(SELF_IDENTIFIER);
+
+				var result = client.fetchMessages(processMessage);
+
+				assertThat(result).isEmpty();
+			}
+		}
+	}
+
+	@DisplayName("fetch messages for client identifier")
+	@Nested
+	class TestFetchMessagesForClientIdentifier {
+
+		@Mock
+		private FetchMessageParameter parameter;
+
+		@BeforeEach
+		void mock() {
+			when(parameter.clientIdentifier()).thenReturn(SELF_IDENTIFIER);
+		}
+
+		@DisplayName("with successful listing")
+		@Nested
+		class TestWithSuccessfulListing {
+
+			@Mock
+			private FetchMessageParameter nextParameter;
+
+			@BeforeEach
+			void mock() {
+				when(service.getStatusList(SELF_IDENTIFIER)).thenReturn(Optional.of(listing));
+				doReturn(transportReports).when(client).fetchMessagesForListing(listing.messages(), parameter);
+			}
+
+			@DisplayName("should return transport reports if no more messages available\"")
+			@Test
+			void shouldReturnTransportReportsIfNoMoreMessagesAvailable() {
+				doReturn(false).when(client).checkMoreMessagesAvailable(listing, transportReports);
+
+				var result = client.fetchMessagesForClientIdentifier(parameter).toList();
+
+				assertThat(result)
+						.extracting(XtaTransportReport::metaData)
+						.extracting(XtaMessageMetaData::messageId)
+						.containsExactly(MESSAGE_ID, MESSAGE_ID2, MESSAGE_ID3);
+			}
+
+			@DisplayName("should return concatenated transport reports if more messages available")
+			@Test
+			void shouldReturnConcatenatedTransportReportsIfMoreMessagesAvailable() {
+				var messageId4 = "messageId4";
+				var additionalTransportReport = XtaTransportReportTestFactory.createBuilder()
+						.metaData(XtaMessageMetaDataTestFactory.createBuilder()
+								.messageId(messageId4)
+								.build())
+						.build();
+				doReturn(true).when(client).checkMoreMessagesAvailable(listing, transportReports);
+				doReturn(nextParameter).when(parameter).withViewedMessageIdsFrom(listing.messages());
+				doReturn(Stream.of(additionalTransportReport)).when(client).fetchMessagesForClientIdentifier(nextParameter);
+
+				var result = client.fetchMessagesForClientIdentifier(parameter).toList();
+
+				assertThat(result)
+						.extracting(XtaTransportReport::metaData)
+						.extracting(XtaMessageMetaData::messageId)
+						.containsExactly(MESSAGE_ID, MESSAGE_ID2, MESSAGE_ID3, messageId4);
+			}
+		}
+
+		@DisplayName("with missing listing")
+		@Nested
+		class TestWithMissingListing {
+
+			@BeforeEach
+			void mock() {
+				when(service.getStatusList(SELF_IDENTIFIER)).thenReturn(Optional.empty());
+			}
+
+			@DisplayName("should return empty stream")
+			@Test
+			void shouldReturnEmptyStream() {
+				var result = client.fetchMessagesForClientIdentifier(parameter).toList();
+
+				assertThat(result).isEmpty();
+			}
+		}
+	}
+
+	@DisplayName("check more messages available")
+	@Nested
+	class TestCheckMoreMessagesAvailable {
+
+		@DisplayName("with no extra pending messages")
+		@Nested
+		class TestWithNoExtraPendingMessages {
+			@DisplayName("should return false")
+			@Test
+			void shouldReturnFalse() {
+				doReturn(false).when(client).checkExtraPendingMessagesAvailable(listing);
+
+				var result = client.checkMoreMessagesAvailable(listing, transportReports);
+
+				assertThat(result).isFalse();
+			}
+		}
+
+		@DisplayName("with extra pending messages but no message closed")
+		@Nested
+		class TestWithExtraPendingMessagesButNoMessageClosed {
+			@DisplayName("should return false")
+			@Test
+			void shouldReturnFalse() {
+				doReturn(true).when(client).checkExtraPendingMessagesAvailable(listing);
+				doReturn(false).when(client).checkSomeMessageHasBeenClosed(transportReports);
+
+				var result = client.checkMoreMessagesAvailable(listing, transportReports);
+
+				assertThat(result).isFalse();
+			}
+		}
+	}
+
+	@DisplayName("check extra pending messages available")
+	@Nested
+	class TestCheckExtraPendingMessagesAvailable {
+		@DisplayName("should return true if more pending than received")
 		@Test
-		@SneakyThrows
-		void shouldCallCheckAccountActive() {
-			client.getMessagesMetadata(SELF_IDENTIFIER_VALUE);
+		void shouldReturnTrueIfMorePendingThanReceived() {
+			var result = client.checkExtraPendingMessagesAvailable(listing);
 
-			verify(service).checkAccountActive(SELF_IDENTIFIER);
+			assertThat(result).isTrue();
 		}
 
-		@DisplayName("should return get status list response")
+		@DisplayName("should return false if no more pending than received")
 		@Test
-		@SneakyThrows
-		void shouldReturnGetStatusListResponse() {
-			var result = client.getMessagesMetadata(SELF_IDENTIFIER_VALUE);
+		void shouldReturnFalseIfNoMorePendingThanReceived() {
+			var listingWithNoExtraPendingMessages = XtaMessageMetaDataListingTestFactory.createBuilder()
+					.pendingMessageCount(BigInteger.valueOf(listing.messages().size()))
+					.build();
+
+			var result = client.checkExtraPendingMessagesAvailable(listingWithNoExtraPendingMessages);
 
-			assertThat(result).isEqualTo(xtaMessageMetaDataListing);
+			assertThat(result).isFalse();
 		}
+	}
+
+	@DisplayName("check some message has been closed")
+	@Nested
+	class TestCheckSomeMessageHasBeenClosed {
+		@DisplayName("should return false if no message has green status")
+		@Test
+		void shouldReturnFalseIfNoMessageHasGreenStatus() {
+			var result = client.checkSomeMessageHasBeenClosed(transportReports);
+
+			assertThat(result).isFalse();
+		}
+
+		@DisplayName("should log warning if no message has been closed")
+		@Test
+		void shouldLogWarningIfNoMessageHasBeenClosed() {
+			client.checkSomeMessageHasBeenClosed(transportReports);
+
+			verify(client).logWarnForNoMessageClosed();
+		}
+
+		@DisplayName("should return true if some message has green status")
+		@Test
+		void shouldReturnTrueIfSomeMessageHasGreenStatus() {
+			var transportReportsWithGreenStatus = Stream.concat(
+					transportReports.stream(),
+					Stream.of(XtaTransportReportTestFactory.createBuilder()
+							.status(XtaMessageStatus.GREEN)
+							.build())
+			).toList();
 
+			var result = client.checkSomeMessageHasBeenClosed(transportReportsWithGreenStatus);
+
+			assertThat(result).isTrue();
+		}
 	}
 
-	@DisplayName("get next messages meta data")
+	@DisplayName("fetch messages for listing")
 	@Nested
-	class TestGetNextMessagesMetaData {
+	class TestFetchMessagesForListing {
 
 		@Mock
-		XtaMessageMetaDataListing xtaMessageMetaDataListing;
+		private FetchMessageParameter parameter;
 
-		@BeforeEach
-		@SneakyThrows
-		void mock() {
-			doReturn(SELF_IDENTIFIER).when(client).deriveIdentifier(SELF_IDENTIFIER_VALUE);
-			when(service.getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS)).thenReturn(xtaMessageMetaDataListing);
-			when(config.getMaxListItems()).thenReturn(MAX_LIST_ITEMS);
+		@Captor
+		private ArgumentCaptor<XtaMessageMetaData> messageMetaDataArgumentCaptor;
+
+		@DisplayName("with messages which should not be fetched")
+		@Nested
+		class TestWithMessagesWhichShouldNotBeFetched {
+
+			@BeforeEach
+			void mock() {
+				doReturn(false).when(client).checkMessageShouldBeFetched(any(), any());
+			}
+
+			@DisplayName("should check if message has already been viewed")
+			@Test
+			void shouldCheckIfMessageHasAlreadyBeenViewed() {
+				client.fetchMessagesForListing(listing.messages(), parameter);
+
+				verify(client, times(3)).checkMessageShouldBeFetched(messageMetaDataArgumentCaptor.capture(), eq(parameter));
+				assertThat(messageMetaDataArgumentCaptor.getAllValues())
+						.containsExactlyElementsOf(listing.messages());
+			}
 		}
 
-		@DisplayName("should return get status list response")
+		@DisplayName("with messages which should be fetched")
+		@Nested
+		class TestWithMessagesWhichShouldBeFetched {
+
+			@BeforeEach
+			void mock() {
+				doReturn(true).when(client).checkMessageShouldBeFetched(any(), any());
+				doAnswer(args -> {
+					XtaMessageMetaData metaData = args.getArgument(0);
+					assertThat(metaData).isNotNull();
+					return getTestTransportReportByMessageId(metaData.messageId());
+				}).when(client).fetchMessage(any(), any());
+			}
+
+			private Optional<XtaTransportReport> getTestTransportReportByMessageId(String messageId) {
+				assertThat(messageId).isNotNull();
+				return transportReports.stream().filter(d -> {
+					assertThat(d.metaData().messageId()).isNotNull();
+					return d.metaData().messageId().equals(messageId);
+				}).findFirst();
+			}
+
+			@DisplayName("should return transport reports")
+			@Test
+			void shouldReturnTransportReports() {
+				var result = client.fetchMessagesForListing(listing.messages(), parameter);
+
+				assertThat(result).containsExactlyElementsOf(transportReports);
+			}
+		}
+	}
+
+	@DisplayName("check message should be fetched")
+	@Nested
+	class TestCheckMessageShouldBeFetched {
+		@Mock
+		private FetchMessageParameter parameter;
+
+		@Mock
+		private XtaMessageMetaData messageMetaData;
+
+		@DisplayName("should return false if message already viewed")
 		@Test
-		@SneakyThrows
-		void shouldReturnGetStatusListResponse() {
-			var result = client.getNextMessagesMetadata(SELF_IDENTIFIER_VALUE);
+		void shouldReturnFalseIfMessageAlreadyViewed() {
+			when(parameter.hasMessageAlreadyBeenViewed(messageMetaData)).thenReturn(true);
 
-			assertThat(result).isEqualTo(xtaMessageMetaDataListing);
+			var result = checkMessageShouldBeFetched();
+
+			assertThat(result).isFalse();
+		}
+
+		@DisplayName("with message not viewed")
+		@Nested
+		class TestWithMessageNotViewed {
+
+			@BeforeEach
+			void mock() {
+				when(parameter.hasMessageAlreadyBeenViewed(messageMetaData)).thenReturn(false);
+			}
+
+			@DisplayName("should return false if message is not supported")
+			@Test
+			void shouldReturnFalseIfMessageIsNotSupported() {
+				doReturn(false).when(client).isMessageSupported(messageMetaData);
+
+				var result = checkMessageShouldBeFetched();
+
+				assertThat(result).isFalse();
+			}
+
+			@DisplayName("should return true if message is supported")
+			@Test
+			void shouldReturnTrueIfMessageIsSupported() {
+				doReturn(true).when(client).isMessageSupported(messageMetaData);
+
+				var result = checkMessageShouldBeFetched();
+
+				assertThat(result).isTrue();
+			}
+		}
+
+		private boolean checkMessageShouldBeFetched() {
+			return client.checkMessageShouldBeFetched(messageMetaData, parameter);
 		}
 	}
 
-	@DisplayName("derive identifier")
+	@DisplayName("is message supported")
 	@Nested
-	class TestDeriveIdentifier {
+	class TestIsMessageSupported {
+
+		@Mock
+		private Predicate<XtaMessageMetaData> isSupportedPredicate;
 
-		@DisplayName("should use value")
+		@Mock
+		private XtaMessageMetaData messageMetaData;
+
+		@DisplayName("should return true if predicate is null")
 		@Test
-		void shouldUseValue() {
-			when(config.getClientIdentifiers()).thenReturn(List.of(SELF_IDENTIFIER2, SELF_IDENTIFIER));
+		void shouldReturnTrueIfPredicateIsNull() {
+			when(config.getIsMessageSupported()).thenReturn(null);
 
-			var result = client.deriveIdentifier(SELF_IDENTIFIER_VALUE);
+			var result = client.isMessageSupported(messageMetaData);
 
-			assertThat(result.value()).isEqualTo(SELF_IDENTIFIER_VALUE);
+			assertThat(result).isTrue();
 		}
 
-		@DisplayName("should throw when unknown")
+		@DisplayName("should return predicate value")
+		@ParameterizedTest
+		@ValueSource(booleans = { true, false })
+		void shouldReturnPredicateValue(boolean value) {
+			when(config.getIsMessageSupported()).thenReturn(isSupportedPredicate);
+			when(isSupportedPredicate.test(messageMetaData)).thenReturn(value);
+
+			var result = client.isMessageSupported(messageMetaData);
+
+			assertThat(result).isEqualTo(value);
+		}
+	}
+
+	@DisplayName("fetch message")
+	@Nested
+	class TestFetchMessage {
+
+		@Mock
+		private XtaMessageMetaData messageMetaData;
+
+		@Mock
+		private FetchMessageParameter parameter;
+
+		@Mock
+		private XtaMessage message;
+
+		@Mock
+		private XtaTransportReport transportReport;
+
+		@DisplayName("should return transport report")
 		@Test
-		void shouldThrowWhenUnknown() {
-			when(config.getClientIdentifiers()).thenReturn(List.of(SELF_IDENTIFIER2));
+		void shouldReturnTransportReport() {
+			when(service.getMessage(messageMetaData)).thenReturn(Optional.of(message));
+			doReturn(Optional.of(transportReport)).when(client).processMesssageAndFetchTransportReport(message, parameter);
 
-			assertThatThrownBy(() -> client.deriveIdentifier(SELF_IDENTIFIER_VALUE))
-					.isInstanceOf(IllegalArgumentException.class)
-					.hasMessage("Unknown identifier: " + SELF_IDENTIFIER_VALUE);
+			var result = client.fetchMessage(messageMetaData, parameter);
+
+			assertThat(result).contains(transportReport);
 		}
 	}
 
-	@DisplayName("get message")
+	@DisplayName("process message and fetch transport report")
 	@Nested
-	class TestGetMessage {
+	class TestProcessMessageAndFetchTransportReport {
+
+		@Mock
+		private XtaMessageMetaData messageMetaData;
 
 		@Mock
-		XtaMessage xtaMessage;
+		private FetchMessageParameter parameter;
 
 		@Mock
-		XtaTransportReport xtaTransportReport;
+		private Consumer<XtaMessage> processMessageConsumer;
+
+		@Mock
+		private XtaMessage message;
+
+		@Mock
+		private XtaTransportReport transportReport;
 
 		@BeforeEach
-		@SneakyThrows
 		void mock() {
-			doReturn(SELF_IDENTIFIER).when(client).deriveIdentifier(SELF_IDENTIFIER_VALUE);
-			when(service.getMessage(MESSAGE_ID, SELF_IDENTIFIER)).thenReturn(xtaMessage);
-			when(service.getTransportReport(MESSAGE_ID, SELF_IDENTIFIER)).thenReturn(xtaTransportReport);
+			when(message.metaData()).thenReturn(messageMetaData);
+			when(parameter.processMessage()).thenReturn(processMessageConsumer);
+			doNothing().when(client).processMessageAndCloseIfNoException(any(), any());
+			when(service.getTransportReport(messageMetaData)).thenReturn(Optional.of(transportReport));
 		}
 
-		@DisplayName("should call close")
+		@DisplayName("should call process message and close if no exception")
 		@Test
-		@SneakyThrows
-		void shouldCallClose() {
-			client.getMessage(SELF_IDENTIFIER_VALUE, MESSAGE_ID);
+		void shouldCallProcessMessageAndCloseIfNoException() {
+			client.processMesssageAndFetchTransportReport(message, parameter);
 
-			verify(service).close(MESSAGE_ID, SELF_IDENTIFIER);
+			verify(client).processMessageAndCloseIfNoException(message, processMessageConsumer);
 		}
 
-		@DisplayName("should return with message")
+		@DisplayName("should return")
 		@Test
-		@SneakyThrows
 		void shouldReturn() {
-			var result = client.getMessage(SELF_IDENTIFIER_VALUE, MESSAGE_ID);
+			var result = client.processMesssageAndFetchTransportReport(message, parameter);
 
-			assertThat(result.message()).isEqualTo(xtaMessage);
+			assertThat(result).contains(transportReport);
 		}
+	}
+
+	@DisplayName("process message and close if no exception")
+	@Nested
+	class TestProcessMessageAndCloseIfNoException {
+
+		@Mock
+		private Consumer<XtaMessage> processMessageConsumer;
 
-		@DisplayName("should return with transport report")
+		@Mock
+		private XtaMessage message;
+
+		@BeforeEach
+		void mock() {
+			when(message.metaData()).thenReturn(listing.messages().getFirst());
+		}
+
+		@DisplayName("should consume message")
 		@Test
-		@SneakyThrows
-		void shouldReturnWithTransportReport() {
-			var result = client.getMessage(SELF_IDENTIFIER_VALUE, MESSAGE_ID);
+		void shouldConsumeMessage() {
+			processMessageAndCloseIfNoException();
+
+			verify(processMessageConsumer).accept(message);
+		}
+
+		@DisplayName("should close message")
+		@Test
+		void shouldCloseMessage() {
+			processMessageAndCloseIfNoException();
 
-			assertThat(result.transportReport()).isEqualTo(xtaTransportReport);
+			verify(service).closeMessage(message);
+		}
+
+		@DisplayName("with runtime exception")
+		@Nested
+		class TestWithRuntimeException {
+
+			@Mock
+			private RuntimeException exception;
+
+			@BeforeEach
+			void mock() {
+				doThrow(exception).when(processMessageConsumer).accept(message);
+			}
+
+			@DisplayName("should log error")
+			@Test
+			void shouldLogError() {
+				processMessageAndCloseIfNoException();
+
+				verify(client).logErrorForMessageProcessingFailure(MESSAGE_ID, exception);
+			}
+
+			@DisplayName("should not close message")
+			@Test
+			void shouldNotCloseMessage() {
+				processMessageAndCloseIfNoException();
+
+				verify(service, times(0)).closeMessage(any());
+			}
+		}
+
+		private void processMessageAndCloseIfNoException() {
+			client.processMessageAndCloseIfNoException(message, processMessageConsumer);
 		}
 	}
 
@@ -172,80 +584,91 @@ class XtaClientTest {
 	class TestSendMessage {
 
 		@Mock
-		XtaMessage xtaMessageWithoutMessageId;
-
-		@Mock
-		XtaMessageMetaData xtaMessageMetaDataWithoutMessageId;
-
-		@Mock
-		XtaMessage xtaMessage;
-
-		@Mock
-		XtaTransportReport xtaTransportReport;
+		private XtaTransportReport transportReport;
 
 		@BeforeEach
-		@SneakyThrows
 		void mock() {
-			when(xtaMessageWithoutMessageId.metaData()).thenReturn(xtaMessageMetaDataWithoutMessageId);
-			when(xtaMessageMetaDataWithoutMessageId.authorIdentifier()).thenReturn(AUTHOR_IDENTIFIER);
-			when(xtaMessageMetaDataWithoutMessageId.readerIdentifier()).thenReturn(READER_IDENTIFIER);
-			when(xtaMessageMetaDataWithoutMessageId.service()).thenReturn(MESSAGE_SERVICE);
-
-			when(service.createMessageId(AUTHOR_IDENTIFIER)).thenReturn(MESSAGE_ID);
-			doReturn(xtaMessage).when(client).createXtaMessageWithMessageId(xtaMessageWithoutMessageId, MESSAGE_ID);
-			when(service.getTransportReport(MESSAGE_ID, AUTHOR_IDENTIFIER)).thenReturn(xtaTransportReport);
+			doNothing().when(client).throwExceptionIfAccountInactive(any());
+			doNothing().when(client).throwExceptionIfServiceNotAvailable(any());
+			when(service.sendMessage(message)).thenReturn(transportReport);
 		}
 
 		@DisplayName("should call checkAccountActive")
 		@Test
 		@SneakyThrows
 		void shouldCallCheckAccountActive() {
-			client.sendMessage(xtaMessageWithoutMessageId);
+			sendMessage();
 
-			verify(service).checkAccountActive(AUTHOR_IDENTIFIER);
+			verify(client).throwExceptionIfAccountInactive(AUTHOR_IDENTIFIER);
 		}
 
 		@DisplayName("should call lookup service")
 		@Test
 		@SneakyThrows
 		void shouldCallLookupService() {
-			client.sendMessage(xtaMessageWithoutMessageId);
+			sendMessage();
 
-			verify(service).lookupService(MESSAGE_SERVICE, READER_IDENTIFIER, AUTHOR_IDENTIFIER);
+			verify(client).throwExceptionIfServiceNotAvailable(message.metaData());
 		}
 
-		@DisplayName("should call send message")
+		@DisplayName("should return")
 		@Test
-		@SneakyThrows
-		void shouldCallSendMessage() {
-			client.sendMessage(xtaMessageWithoutMessageId);
+		void shouldReturn() {
+			var result = sendMessage();
 
-			verify(service).sendMessage(xtaMessage);
+			assertThat(result).isEqualTo(transportReport);
 		}
 
-		@DisplayName("should return with transport report")
+		private XtaTransportReport sendMessage() {
+			return client.sendMessage(message);
+		}
+	}
+
+	@DisplayName("throw exception if service not available")
+	@Nested
+	class TestThrowExceptionIfServiceNotAvailable {
+
+		@DisplayName("should call lookupService")
 		@Test
-		@SneakyThrows
-		void shouldReturnWithTransportReport() {
-			var result = client.sendMessage(xtaMessageWithoutMessageId);
+		void shouldCallLookupService() {
+			doReturn(true).when(service).lookupService(any());
+
+			client.throwExceptionIfServiceNotAvailable(message.metaData());
 
-			assertThat(result).isEqualTo(xtaTransportReport);
+			verify(service).lookupService(message.metaData());
+		}
+
+		@DisplayName("should throw exception if service not available")
+		@Test
+		void shouldThrowExceptionIfServiceNotAvailable() {
+			doReturn(false).when(service).lookupService(message.metaData());
+
+			assertThatThrownBy(() -> client.throwExceptionIfServiceNotAvailable(message.metaData()))
+					.isInstanceOf(ClientRuntimeException.class);
 		}
 	}
 
-	@DisplayName("create xta message with message id")
+	@DisplayName("throw exception if account inactive")
 	@Nested
-	class TestCreateXtaMessageWithMessageId {
-		private final XtaMessage xtaMessage = XtaMessageTestFactory.create();
+	class TestThrowExceptionIfAccountInactive {
 
-		@DisplayName("should set message id")
+		@DisplayName("should call checkAccountActive")
 		@Test
-		void shouldSetMessageId() {
-			var newMessageId = "newMessageId";
+		void shouldCallCheckAccountActive() {
+			doReturn(true).when(service).checkAccountActive(any());
 
-			var result = client.createXtaMessageWithMessageId(xtaMessage, newMessageId);
+			client.throwExceptionIfAccountInactive(SELF_IDENTIFIER);
+
+			verify(service).checkAccountActive(SELF_IDENTIFIER);
+		}
+
+		@DisplayName("should throw exception if account inactive")
+		@Test
+		void shouldThrowExceptionIfAccountInactive() {
+			doReturn(false).when(service).checkAccountActive(AUTHOR_IDENTIFIER);
 
-			assertThat(result.metaData().messageId()).isEqualTo(newMessageId);
+			assertThatThrownBy(() -> client.throwExceptionIfAccountInactive(AUTHOR_IDENTIFIER))
+					.isInstanceOf(ClientRuntimeException.class);
 		}
 	}
 
diff --git a/src/test/java/de/ozgcloud/xta/client/core/XtaClientServiceTest.java b/src/test/java/de/ozgcloud/xta/client/core/XtaClientServiceTest.java
new file mode 100644
index 0000000..266ffd9
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/core/XtaClientServiceTest.java
@@ -0,0 +1,427 @@
+package de.ozgcloud.xta.client.core;
+
+import static de.ozgcloud.xta.client.factory.MessageMetaDataTestFactory.*;
+import static de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import de.ozgcloud.xta.client.config.XtaClientConfig;
+import de.ozgcloud.xta.client.exception.ClientRuntimeException;
+import de.ozgcloud.xta.client.factory.XtaMessageMetaDataTestFactory;
+import de.ozgcloud.xta.client.factory.XtaMessageTestFactory;
+import de.ozgcloud.xta.client.factory.XtaTransportReportTestFactory;
+import de.ozgcloud.xta.client.model.XtaMessage;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
+import de.ozgcloud.xta.client.model.XtaTransportReport;
+import genv3.de.xoev.transport.xta.x211.PermissionDeniedException;
+import genv3.de.xoev.transport.xta.x211.XTAWSTechnicalProblemException;
+import lombok.SneakyThrows;
+
+class XtaClientServiceTest {
+
+	@Mock
+	private WrappedXtaService wrapper;
+
+	@Mock
+	private XtaClientConfig config;
+
+	@Spy
+	@InjectMocks
+	private XtaClientService service;
+
+	private final XtaMessageMetaData messageMetaData = XtaMessageMetaDataTestFactory.create();
+	private final XtaMessage message = XtaMessageTestFactory.create();
+	private final XtaTransportReport transportReport = XtaTransportReportTestFactory.create();
+
+	@DisplayName("get transport report")
+	@Nested
+	class TestGetTransportReport {
+
+		@Mock
+		private ClientRuntimeException exception;
+
+		@DisplayName("should return")
+		@Test
+		void shouldReturn() {
+			doReturn(transportReport).when(service).getTransportReportOrThrowException(messageMetaData);
+
+			var result = service.getTransportReport(messageMetaData);
+
+			assertThat(result).contains(transportReport);
+		}
+
+		@DisplayName("should return empty if exception")
+		@Test
+		void shouldReturnEmptyIfException() {
+			doThrow(exception).when(service).getTransportReportOrThrowException(messageMetaData);
+
+			var result = service.getTransportReport(messageMetaData);
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should log error if exception")
+		@Test
+		void shouldLogErrorIfException() {
+			doThrow(exception).when(service).getTransportReportOrThrowException(messageMetaData);
+
+			service.getTransportReport(messageMetaData);
+
+			verify(service).logError(anyString(), eq(exception));
+		}
+	}
+
+	@DisplayName("close message")
+	@Nested
+	class TestCloseMessage {
+		@Mock
+		private XTAWSTechnicalProblemException xtaWSTechnicalProblemException;
+
+		@DisplayName("should call close")
+		@Test
+		@SneakyThrows
+		void shouldCallClose() {
+			service.closeMessage(message);
+
+			verify(wrapper).close(MESSAGE_ID, READER_IDENTIFIER);
+		}
+
+		@DisplayName("should log error if exception")
+		@Test
+		@SneakyThrows
+		void shouldLogErrorIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).close(MESSAGE_ID, READER_IDENTIFIER);
+
+			service.closeMessage(message);
+
+			verify(service).logError(anyString(), xtaWSTechnicalProblemException);
+		}
+
+	}
+
+	@DisplayName("get message")
+	@Nested
+	class TestGetMessage {
+
+		@Mock
+		private XTAWSTechnicalProblemException xtaWSTechnicalProblemException;
+
+		@DisplayName("should return")
+		@Test
+		@SneakyThrows
+		void shouldReturn() {
+			doReturn(message).when(wrapper).getMessage(MESSAGE_ID, READER_IDENTIFIER);
+
+			var result = service.getMessage(messageMetaData);
+
+			assertThat(result).contains(message);
+		}
+
+		@DisplayName("should return empty if exception")
+		@Test
+		@SneakyThrows
+		void shouldReturnEmptyIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).getMessage(MESSAGE_ID, READER_IDENTIFIER);
+
+			var result = service.getMessage(messageMetaData);
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should log error if exception")
+		@Test
+		@SneakyThrows
+		void shouldLogErrorIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).getMessage(MESSAGE_ID, READER_IDENTIFIER);
+
+			service.getMessage(messageMetaData);
+
+			verify(service).logError(anyString(), xtaWSTechnicalProblemException);
+		}
+	}
+
+	@DisplayName("get status list")
+	@Nested
+	class TestGetStatusList {
+
+		@Mock
+		private XTAWSTechnicalProblemException xtaWSTechnicalProblemException;
+
+		@BeforeEach
+		void mock() {
+			when(config.getMaxListItems()).thenReturn(MAX_LIST_ITEMS);
+		}
+
+		@DisplayName("should return")
+		@Test
+		@SneakyThrows
+		void shouldReturn() {
+			doReturn(messageMetaData).when(wrapper).getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS);
+
+			var result = service.getStatusList(SELF_IDENTIFIER);
+
+			assertThat(result).isEqualTo(messageMetaData);
+		}
+
+		@DisplayName("should return empty if exception")
+		@Test
+		@SneakyThrows
+		void shouldReturnEmptyIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS);
+
+			var result = service.getStatusList(SELF_IDENTIFIER);
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should log error if exception")
+		@Test
+		@SneakyThrows
+		void shouldLogErrorIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS);
+
+			service.getStatusList(SELF_IDENTIFIER);
+
+			verify(service).logError(anyString(), xtaWSTechnicalProblemException);
+		}
+	}
+
+	@DisplayName("check account active")
+	@Nested
+	class TestCheckAccountActive {
+
+		@DisplayName("should call check account active")
+		@Test
+		@SneakyThrows
+		void shouldCallCheckAccountActive() {
+			checkAccountActive();
+
+			verify(wrapper).checkAccountActive(SELF_IDENTIFIER);
+		}
+
+		@DisplayName("should return true if no exception")
+		@Test
+		void shouldReturnTrueIfNoException() {
+			var result = checkAccountActive();
+
+			assertThat(result).isTrue();
+		}
+
+		@DisplayName("should return false if permission denied exception")
+		@Test
+		@SneakyThrows
+		void shouldReturnFalseIfPermissionDeniedException() {
+			doThrow(new PermissionDeniedException()).when(wrapper).checkAccountActive(any());
+
+			var result = checkAccountActive();
+
+			assertThat(result).isFalse();
+		}
+
+		@DisplayName("should throw on technical problem exception")
+		@Test
+		@SneakyThrows
+		void shouldThrowOnTechnicalProblemException() {
+			var technicalException = new XTAWSTechnicalProblemException();
+			doThrow(technicalException).when(wrapper).checkAccountActive(any());
+
+			assertThatThrownBy(this::checkAccountActive)
+					.isInstanceOf(ClientRuntimeException.class)
+					.hasCause(technicalException);
+		}
+
+		private boolean checkAccountActive() {
+			return service.checkAccountActive(SELF_IDENTIFIER);
+		}
+	}
+
+	@DisplayName("send message")
+	@Nested
+	class TestSendMessage {
+
+		@Mock
+		private XtaMessage messageWithoutMessageId;
+
+		@Mock
+		private XTAWSTechnicalProblemException exception;
+
+		@BeforeEach
+		@SneakyThrows
+		void mock() {
+			doReturn(message).when(service).getXtaMessageWithMessageId(message);
+		}
+
+		@DisplayName("should call send message")
+		@Test
+		@SneakyThrows
+		void shouldCallSendMessage() {
+			sendMessage();
+
+			verify(wrapper).sendMessage(message);
+		}
+
+		@DisplayName("should return with transport report")
+		@Test
+		@SneakyThrows
+		void shouldReturnWithTransportReport() {
+			doReturn(transportReport).when(service).getTransportReportOrThrowException(message.metaData());
+
+			var result = sendMessage();
+
+			assertThat(result).isEqualTo(transportReport);
+		}
+
+		@DisplayName("should throw client runtime exception if send message fails")
+		@Test
+		@SneakyThrows
+		void shouldThrowClientRuntimeExceptionIfSendMessageFails() {
+			doThrow(exception).when(wrapper).sendMessage(message);
+
+			assertThatThrownBy(this::sendMessage)
+					.isInstanceOf(ClientRuntimeException.class)
+					.hasCause(exception);
+		}
+
+		private XtaTransportReport sendMessage() {
+			return service.sendMessage(messageWithoutMessageId);
+		}
+	}
+
+	@DisplayName("get xta message with message id")
+	@Nested
+	class TestGetXtaMessageWithMessageId {
+
+		@Mock
+		private XtaMessage messageWithMessageId;
+
+		@Mock
+		private XTAWSTechnicalProblemException exception;
+
+		@DisplayName("should return message")
+		@Test
+		@SneakyThrows
+		void shouldReturnMessage() {
+			doReturn(MESSAGE_ID2).when(wrapper).createMessageId(AUTHOR_IDENTIFIER);
+			doReturn(messageWithMessageId).when(service).createXtaMessageWithMessageId(message, MESSAGE_ID2);
+
+			var result = service.getXtaMessageWithMessageId(message);
+
+			assertThat(result).isEqualTo(messageWithMessageId);
+		}
+
+		@DisplayName("should throw client runtime exception if create message fails")
+		@Test
+		@SneakyThrows
+		void shouldThrowClientRuntimeExceptionIfCreateMessageFails() {
+			doThrow(exception).when(wrapper).createMessageId(AUTHOR_IDENTIFIER);
+
+			assertThatThrownBy(() -> service.getXtaMessageWithMessageId(message))
+					.isInstanceOf(ClientRuntimeException.class)
+					.hasCause(exception);
+		}
+	}
+
+	@DisplayName("get transport report or throw exception")
+	@Nested
+	class TestGetTransportReportOrThrowException {
+		@Mock
+		private XTAWSTechnicalProblemException exception;
+
+		@DisplayName("should return")
+		@Test
+		@SneakyThrows
+		void shouldReturn() {
+			when(wrapper.getTransportReport(MESSAGE_ID, AUTHOR_IDENTIFIER)).thenReturn(transportReport);
+
+			var result = service.getTransportReportOrThrowException(messageMetaData);
+
+			assertThat(result).isEqualTo(transportReport);
+		}
+
+		@DisplayName("should throw client exception if exception")
+		@Test
+		@SneakyThrows
+		void shouldThrowClientExceptionIfException() {
+			when(wrapper.getTransportReport(MESSAGE_ID, AUTHOR_IDENTIFIER)).thenThrow(exception);
+
+			assertThatThrownBy(() -> service.getTransportReportOrThrowException(messageMetaData))
+					.isInstanceOf(ClientRuntimeException.class)
+					.hasCause(exception);
+		}
+	}
+
+	@DisplayName("create xta message with message id")
+	@Nested
+	class TestCreateXtaMessageWithMessageId {
+		private final XtaMessage xtaMessage = XtaMessageTestFactory.create();
+
+		@DisplayName("should set message id")
+		@Test
+		void shouldSetMessageId() {
+			var newMessageId = "newMessageId";
+
+			var result = service.createXtaMessageWithMessageId(xtaMessage, newMessageId);
+
+			assertThat(result.metaData().messageId()).isEqualTo(newMessageId);
+		}
+	}
+
+	@DisplayName("lookup service")
+	@Nested
+	class TestLookupService {
+		@Mock
+		private XTAWSTechnicalProblemException exception;
+
+		@DisplayName("should return lookup result")
+		@ParameterizedTest
+		@ValueSource(booleans = { true, false })
+		@SneakyThrows
+		void shouldReturnLookupResult(boolean lookupResult) {
+			when(wrapper.lookupService(MESSAGE_SERVICE, READER_IDENTIFIER, AUTHOR_IDENTIFIER)).thenReturn(lookupResult);
+
+			var result = lookupService();
+
+			assertThat(result).isEqualTo(lookupResult);
+		}
+
+		@DisplayName("should return false if exception")
+		@Test
+		@SneakyThrows
+		void shouldReturnFalseIfException() {
+			when(wrapper.lookupService(MESSAGE_SERVICE, READER_IDENTIFIER, AUTHOR_IDENTIFIER)).thenThrow(exception);
+
+			var result = lookupService();
+
+			assertThat(result).isFalse();
+		}
+
+		@DisplayName("should log warning if exception")
+		@Test
+		@SneakyThrows
+		void shouldLogWarningIfException() {
+			when(wrapper.lookupService(MESSAGE_SERVICE, READER_IDENTIFIER, AUTHOR_IDENTIFIER)).thenThrow(exception);
+
+			lookupService();
+
+			verify(service).logWarning(anyString(), eq(exception));
+		}
+
+		private boolean lookupService() {
+			return service.lookupService(messageMetaData);
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java b/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java
index 45888f6..4ebb6c6 100644
--- a/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java
+++ b/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java
@@ -5,6 +5,8 @@ import java.util.List;
 import de.ozgcloud.xta.client.XtaClient;
 import de.ozgcloud.xta.client.config.XtaClientConfig;
 import de.ozgcloud.xta.client.core.WrappedXtaService;
+import de.ozgcloud.xta.client.core.XtaClientService;
+import de.ozgcloud.xta.client.exception.ClientRuntimeException;
 import de.ozgcloud.xta.client.model.XtaIdentifier;
 import de.ozgcloud.xta.client.model.XtaMessage;
 import de.ozgcloud.xta.client.model.XtaMessageMetaData;
@@ -54,17 +56,17 @@ public class XtaServerSetupExtensionTestUtil {
 			log.info("Sending from author {} to reader {}.", message.metaData().authorIdentifier(), message.metaData().readerIdentifier());
 			var transportReport = client.sendMessage(message);
 			return transportReport.metaData().messageId();
-		} catch (ParameterIsNotValidException e) {
-			logCodeFehlerNummer(e.getFaultInfo().getErrorCode());
-			throw e;
-		} catch (PermissionDeniedException e) {
-			logCodeFehlerNummer(e.getFaultInfo().getErrorCode());
-			throw e;
-		} catch (MessageSchemaViolationException e) {
-			logCodeFehlerNummer(e.getFaultInfo().getErrorCode());
-			throw e;
-		} catch (XTAWSTechnicalProblemException e) {
-			logCodeFehlerNummer(e.getFaultInfo().getErrorCode());
+		} catch (ClientRuntimeException e) {
+			var cause = e.getCause();
+			if (cause instanceof ParameterIsNotValidException) {
+				logCodeFehlerNummer(((ParameterIsNotValidException) cause).getFaultInfo().getErrorCode());
+			} else if (cause instanceof PermissionDeniedException) {
+				logCodeFehlerNummer(((PermissionDeniedException) cause).getFaultInfo().getErrorCode());
+			} else if (cause instanceof MessageSchemaViolationException) {
+				logCodeFehlerNummer(((MessageSchemaViolationException) cause).getFaultInfo().getErrorCode());
+			} else if (cause instanceof XTAWSTechnicalProblemException) {
+				logCodeFehlerNummer(((XTAWSTechnicalProblemException) cause).getFaultInfo().getErrorCode());
+			}
 			throw e;
 		}
 	}
@@ -75,19 +77,26 @@ public class XtaServerSetupExtensionTestUtil {
 
 	@SneakyThrows
 	public static void closeAllMessages(XtaClient client, XtaIdentifier clientId) {
-		var field = XtaClient.class.getDeclaredField("service");
-		field.setAccessible(true);
-		var service = (WrappedXtaService) field.get(client);
+		XtaClientService clientService = getPrivateFieldValue(client, "service");
+		WrappedXtaService wrappedService = getPrivateFieldValue(clientService, "service");
 
-		var result = service.getStatusList(clientId, 100);
+		var result = wrappedService.getStatusList(clientId, 100);
 		var messageIds = result.messages().stream()
 				.map(XtaMessageMetaData::messageId)
 				.toList();
 		for (var messageId : messageIds) {
-			service.close(messageId, clientId);
+			wrappedService.close(messageId, clientId);
 		}
 	}
 
+	@SuppressWarnings("unchecked")
+	@SneakyThrows
+	private static <T> T getPrivateFieldValue(Object object, String fieldName) {
+		var field = object.getClass().getDeclaredField(fieldName);
+		field.setAccessible(true);
+		return (T) field.get(object);
+	}
+
 	@SneakyThrows
 	public static byte[] extractMessageFileContent(XtaMessage xtaMessage) {
 		return xtaMessage.messageFile().content().getInputStream().readAllBytes();
diff --git a/src/test/java/de/ozgcloud/xta/client/factory/XtaMessageMetaDataListingTestFactory.java b/src/test/java/de/ozgcloud/xta/client/factory/XtaMessageMetaDataListingTestFactory.java
new file mode 100644
index 0000000..3da8ca5
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/factory/XtaMessageMetaDataListingTestFactory.java
@@ -0,0 +1,31 @@
+package de.ozgcloud.xta.client.factory;
+
+import static de.ozgcloud.xta.client.factory.MessageMetaDataTestFactory.*;
+
+import java.math.BigInteger;
+import java.util.List;
+
+import de.ozgcloud.xta.client.model.XtaMessageMetaDataListing;
+
+public class XtaMessageMetaDataListingTestFactory {
+
+	public final static BigInteger PENDING_MESSAGES_COUNT = BigInteger.valueOf(7);
+
+	public static XtaMessageMetaDataListing create() {
+		return createBuilder().build();
+	}
+
+	public static XtaMessageMetaDataListing.XtaMessageMetaDataListingBuilder createBuilder() {
+		return XtaMessageMetaDataListing.builder()
+				.messages(List.of(XtaMessageMetaDataTestFactory.create(),
+						XtaMessageMetaDataTestFactory.createBuilder()
+								.messageId(MESSAGE_ID2)
+								.messageTypeCode(MESSAGE_TYPE_CODE2)
+								.build(),
+						XtaMessageMetaDataTestFactory.createBuilder()
+								.messageId(MESSAGE_ID3)
+								.messageTypeCode(MESSAGE_TYPE_CODE3)
+								.build()))
+				.pendingMessageCount(PENDING_MESSAGES_COUNT);
+	}
+}
diff --git a/src/test/java/de/ozgcloud/xta/client/factory/XtaTransportReportTestFactory.java b/src/test/java/de/ozgcloud/xta/client/factory/XtaTransportReportTestFactory.java
new file mode 100644
index 0000000..33d54a7
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/factory/XtaTransportReportTestFactory.java
@@ -0,0 +1,21 @@
+package de.ozgcloud.xta.client.factory;
+
+import static de.ozgcloud.xta.client.factory.TransportReportTestFactory.*;
+
+
+import de.ozgcloud.xta.client.model.XtaMessageStatus;
+import de.ozgcloud.xta.client.model.XtaTransportReport;
+
+public class XtaTransportReportTestFactory {
+
+	public static XtaTransportReport create() {
+		return createBuilder().build();
+	}
+
+	public static XtaTransportReport.XtaTransportReportBuilder createBuilder() {
+		return XtaTransportReport.builder()
+				.reportTime(REPORT_TIME)
+				.metaData(XtaMessageMetaDataTestFactory.create())
+				.status(XtaMessageStatus.OPEN);
+	}
+}
-- 
GitLab