From 6c95fb6e88711af55d67c7dd03f8472002b22ee5 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Wed, 12 Mar 2025 14:32:36 +0100
Subject: [PATCH 01/28] OZG-7573 OZG-7925 expand Mapping from GrpcEignang to
 FormData

---
 .../formdata/AntragstellerTestFactory.java    |   7 +-
 .../common/formdata/FormDataTestFactory.java  |  15 +--
 .../formdata/PostfachAddressTestFactory.java  |   4 +-
 .../ZustaendigeStelleTestFactory.java         |   4 +
 .../eingang/router/GrpcEingangMapper.java     |  18 +--
 .../eingang/router/ServiceKontoMapper.java    |  64 +++++-----
 .../router/GrpcAntragstellerTestFactory.java  |  81 +++++++++++++
 .../router/GrpcEingangHeaderTestFactory.java  |   4 +-
 .../eingang/router/GrpcEingangMapperTest.java | 110 ++++++++++++++----
 .../router/GrpcEingangTestFactory.java        |  57 +++++++++
 .../GrpcIncomingFileGroupTestFactory.java     |   3 +-
 .../router/GrpcIncomingFileTestFactory.java   |   2 +-
 .../GrpcPostfachAddressTestFactory.java       |  33 ++++++
 .../router/GrpcServiceKontoTestFactory.java   |  25 ++++
 .../GrpcZustaendigeStelleTestFactory.java     |  57 +++++++++
 .../router/ServiceKontoMapperTest.java        |  44 ++++++-
 16 files changed, 450 insertions(+), 78 deletions(-)
 create mode 100644 router/src/test/java/de/ozgcloud/eingang/router/GrpcAntragstellerTestFactory.java
 create mode 100644 router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangTestFactory.java
 create mode 100644 router/src/test/java/de/ozgcloud/eingang/router/GrpcPostfachAddressTestFactory.java
 create mode 100644 router/src/test/java/de/ozgcloud/eingang/router/GrpcServiceKontoTestFactory.java
 create mode 100644 router/src/test/java/de/ozgcloud/eingang/router/GrpcZustaendigeStelleTestFactory.java

diff --git a/common/src/test/java/de/ozgcloud/eingang/common/formdata/AntragstellerTestFactory.java b/common/src/test/java/de/ozgcloud/eingang/common/formdata/AntragstellerTestFactory.java
index 4418a4fc7..acb3f7384 100644
--- a/common/src/test/java/de/ozgcloud/eingang/common/formdata/AntragstellerTestFactory.java
+++ b/common/src/test/java/de/ozgcloud/eingang/common/formdata/AntragstellerTestFactory.java
@@ -38,6 +38,9 @@ public class AntragstellerTestFactory {
 	public static final String SUB_VERBOTENE_VEREINIGUNG_KEY = "MitgliedschaftInVerboternerVereinigung";
 	public static final String SUB_VERBOTENE_VEREINIGUNG_VALUE = "true";
 
+	public static final Map<String, Object> DATA = Map.of(GEBIET_BEZEICHNUNG_KEY, GEBIET_BEZEICHNUNG_VALUE,
+			SUB_PERSOENLICHE_EIGNUNG, Map.of(SUB_VERBOTENE_VEREINIGUNG_KEY, SUB_VERBOTENE_VEREINIGUNG_VALUE));
+
 	public static final String VORNAME = "Theo";
 	public static final String NACHNAME = "Test";
 	public static final String GEBURTSNAME = "Toast";
@@ -82,8 +85,6 @@ public class AntragstellerTestFactory {
 				.plz(PLZ)
 				.ort(ORT)
 				.postfachId(POSTFACH_ID)
-				.data(Map.of(GEBIET_BEZEICHNUNG_KEY, GEBIET_BEZEICHNUNG_VALUE,
-						SUB_PERSOENLICHE_EIGNUNG,
-						Map.of(SUB_VERBOTENE_VEREINIGUNG_KEY, SUB_VERBOTENE_VEREINIGUNG_VALUE)));
+				.data(DATA);
 	}
 }
diff --git a/common/src/test/java/de/ozgcloud/eingang/common/formdata/FormDataTestFactory.java b/common/src/test/java/de/ozgcloud/eingang/common/formdata/FormDataTestFactory.java
index 1c5ceaddf..94f756da0 100644
--- a/common/src/test/java/de/ozgcloud/eingang/common/formdata/FormDataTestFactory.java
+++ b/common/src/test/java/de/ozgcloud/eingang/common/formdata/FormDataTestFactory.java
@@ -28,6 +28,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 import de.ozgcloud.eingang.common.formdata.FormData.FormDataControl;
 import lombok.AccessLevel;
@@ -36,6 +37,7 @@ import lombok.NoArgsConstructor;
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class FormDataTestFactory {
 
+	public static final String ID = UUID.randomUUID().toString();
 	public static final String SIMPLE_VALUE_KEY = "kontaktsystemtypid";
 	public static final String SIMPLE_VALUE = "233034600";
 
@@ -54,12 +56,9 @@ public class FormDataTestFactory {
 	public static final Map<String, Object> NESTED_LIST_OBJECTS_ELEMENT_2 = Map.of("o3", "o4");
 	public static final List<Map<String, Object>> NESTED_LIST_WITH_OBJECTS = List.of(NESTED_LIST_OBJECTS_ELEMENT_1, NESTED_LIST_OBJECTS_ELEMENT_2);
 
-	public static final String ATTACHMENT_GROUP_2 = "FileGroup2";
-
 	public static final String VORGANG_ID = "vorgangId";
 
-	public static final List<IncomingFileGroup> ATTACHMENTS = List.of(IncomingFileGroupTestFactory.create(),
-			IncomingFileGroupTestFactory.createBuilder().name(ATTACHMENT_GROUP_2).build());
+	public static final List<IncomingFileGroup> ATTACHMENTS = List.of(IncomingFileGroupTestFactory.create());
 	public static final List<IncomingFile> REPRESENTATIONS = List.of(IncomingFileTestFactory.create());
 
 	public static final FormDataControl FORM_DATA_CONTROL = FormDataControlTestFactory.create();
@@ -76,17 +75,15 @@ public class FormDataTestFactory {
 
 	public static FormData.FormDataBuilder createBuilder() {
 		return FormData.builder()
+				.id(ID)
 				.header(FormHeaderTestFactory.create())
 				.antragsteller(AntragstellerTestFactory.create())
 				.zustaendigeStelle(ZustaendigeStelleTestFactory.create())
 				.control(FORM_DATA_CONTROL)
 				.formData(FORM_DATA)
-				// TODO nach entfernen des zweiten Attachments den Wert auf 1 setzen
-				.numberOfAttachments(2)
-				// TODO zweites Attachment aus der TestFactory entfernen und die entsprechenden
-				// Tests anpassen
+				.numberOfAttachments(ATTACHMENTS.size())
 				.attachments(ATTACHMENTS)
-				.numberOfRepresentations(1)
+				.numberOfRepresentations(REPRESENTATIONS.size())
 				.representations(REPRESENTATIONS);
 	}
 
diff --git a/common/src/test/java/de/ozgcloud/eingang/common/formdata/PostfachAddressTestFactory.java b/common/src/test/java/de/ozgcloud/eingang/common/formdata/PostfachAddressTestFactory.java
index 0e3e2a281..a7d2fa3ce 100644
--- a/common/src/test/java/de/ozgcloud/eingang/common/formdata/PostfachAddressTestFactory.java
+++ b/common/src/test/java/de/ozgcloud/eingang/common/formdata/PostfachAddressTestFactory.java
@@ -25,11 +25,13 @@ package de.ozgcloud.eingang.common.formdata;
 
 import java.util.UUID;
 
+import org.apache.commons.lang3.RandomUtils;
+
 import de.ozgcloud.eingang.common.formdata.ServiceKonto.PostfachAddress;
 
 public class PostfachAddressTestFactory {
 
-	public static final int POSTFACH_ADDRESS_TYPE = 1;
+	public static final int POSTFACH_ADDRESS_TYPE = RandomUtils.insecure().randomInt();
 	public static final String VERSION = "1.0";
 	public static final String POSTFACH_ID = UUID.randomUUID().toString();
 
diff --git a/common/src/test/java/de/ozgcloud/eingang/common/formdata/ZustaendigeStelleTestFactory.java b/common/src/test/java/de/ozgcloud/eingang/common/formdata/ZustaendigeStelleTestFactory.java
index 5b14fe2d5..34adca8f0 100644
--- a/common/src/test/java/de/ozgcloud/eingang/common/formdata/ZustaendigeStelleTestFactory.java
+++ b/common/src/test/java/de/ozgcloud/eingang/common/formdata/ZustaendigeStelleTestFactory.java
@@ -23,6 +23,8 @@
  */
 package de.ozgcloud.eingang.common.formdata;
 
+import com.thedeanda.lorem.LoremIpsum;
+
 public class ZustaendigeStelleTestFactory {
 
 	public static final String ORGANISATIONSEINHEIT_ID = "08150815";
@@ -33,6 +35,7 @@ public class ZustaendigeStelleTestFactory {
 	public static final String HAUSANSCHRIFT_PLZ = "12345";
 	public static final String HAUSANSCHRIFT_ORT = "Musterort";
 	public static final String TELEFON = "0123456789";
+	public static final String BEZEICHNUNG = LoremIpsum.getInstance().getWords(5);
 
 	public static ZustaendigeStelle create() {
 		return createBuilder().build();
@@ -42,6 +45,7 @@ public class ZustaendigeStelleTestFactory {
 		return ZustaendigeStelle.builder() //
 				.organisationseinheitenId(ORGANISATIONSEINHEIT_ID)
 				.email(EMAIL)
+				.bezeichnung(BEZEICHNUNG)
 				.gemeindeSchluessel(GEMEINDE_SCHLUESSEL)
 				.amtlicherRegionalSchluessel(AMTLICHER_REGIONAL_SCHLUESSEL)
 				.hausanschriftStrasse(HAUSANSCHRIFT_STRASSE)
diff --git a/router/src/main/java/de/ozgcloud/eingang/router/GrpcEingangMapper.java b/router/src/main/java/de/ozgcloud/eingang/router/GrpcEingangMapper.java
index eb5d7c414..382c59752 100644
--- a/router/src/main/java/de/ozgcloud/eingang/router/GrpcEingangMapper.java
+++ b/router/src/main/java/de/ozgcloud/eingang/router/GrpcEingangMapper.java
@@ -44,7 +44,6 @@ import de.ozgcloud.vorgang.vorgang.GrpcIncomingFileGroup;
 import de.ozgcloud.vorgang.vorgang.GrpcZustaendigeStelle;
 
 @Mapper(unmappedTargetPolicy = ReportingPolicy.WARN, //
-		unmappedSourcePolicy = ReportingPolicy.WARN, //
 		nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, //
 		nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, //
 		collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, //
@@ -59,8 +58,8 @@ public interface GrpcEingangMapper {
 
 	default ZustaendigeStelle getZustaendigeStelle(FormData formData, Optional<String> organisationsEinheitenId) {
 		return organisationsEinheitenId.flatMap(oeId -> formData.getZustaendigeStelles().stream()
-						.filter(zustaendigeStelle -> zustaendigeStelle.getOrganisationseinheitenId().equals(oeId))
-						.findFirst())
+				.filter(zustaendigeStelle -> zustaendigeStelle.getOrganisationseinheitenId().equals(oeId))
+				.findFirst())
 				.orElseGet(() -> ZustaendigeStelle.builder().build());
 	}
 
@@ -84,13 +83,16 @@ public interface GrpcEingangMapper {
 		return id.toString();
 	}
 
-	// FIXME map representations and attachments
 	@Mapping(target = "attachment", ignore = true)
-	@Mapping(target = "attachments", ignore = true)
 	@Mapping(target = "representation", ignore = true)
-	@Mapping(target = "representations", ignore = true)
-	// TOASK: Wird aktuell nicht gebraucht, trotzdem implementiern?
-	@Mapping(target = "header.serviceKonto", ignore = true)
+	@Mapping(target = "zustaendigeStelles", ignore = true)
+	@Mapping(target = "control", ignore = true)
+	@Mapping(target = "attachments", source = "attachmentsList")
+	@Mapping(target = "representations", source = "representationsList")
+	@Mapping(target = "antragsteller.data", source = "antragsteller.otherData")
 	FormData toFormData(GrpcEingang eingang);
 
+	@Mapping(target = "files", source = "filesList")
+	@Mapping(target = "file", ignore = true)
+	IncomingFileGroup mapFileGroupFromGrpc(GrpcIncomingFileGroup fileGroup);
 }
diff --git a/router/src/main/java/de/ozgcloud/eingang/router/ServiceKontoMapper.java b/router/src/main/java/de/ozgcloud/eingang/router/ServiceKontoMapper.java
index 7986140e7..85b8a1685 100644
--- a/router/src/main/java/de/ozgcloud/eingang/router/ServiceKontoMapper.java
+++ b/router/src/main/java/de/ozgcloud/eingang/router/ServiceKontoMapper.java
@@ -23,55 +23,67 @@
  */
 package de.ozgcloud.eingang.router;
 
-import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.mapstruct.CollectionMappingStrategy;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
+import org.mapstruct.NullValueCheckStrategy;
+import org.mapstruct.NullValuePropertyMappingStrategy;
+import org.mapstruct.ReportingPolicy;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
 
 import de.ozgcloud.eingang.common.formdata.PostfachAddressIdentifier;
 import de.ozgcloud.eingang.common.formdata.ServiceKonto;
 import de.ozgcloud.eingang.common.formdata.StringBasedIdentifier;
-import de.ozgcloud.eingang.common.formdata.ServiceKonto.PostfachAddress;
 import de.ozgcloud.vorgang.common.GrpcObject;
 import de.ozgcloud.vorgang.common.grpc.GrpcObjectMapper;
-import de.ozgcloud.vorgang.vorgang.GrpcPostfachAddress;
 import de.ozgcloud.vorgang.vorgang.GrpcServiceKonto;
 
-@Component
-class ServiceKontoMapper {
+@Mapper(unmappedTargetPolicy = ReportingPolicy.WARN, //
+		nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, //
+		nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, //
+		collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, //
+		uses = { GrpcObjectMapper.class })
+abstract class ServiceKontoMapper {
 
 	static final String DEFAULT_TRUST_LEVEL = "STORK-QAA-Level-1";
 
 	@Autowired
 	private GrpcObjectMapper grpcObjectMapper;
 
-	public GrpcServiceKonto toServiceKonto(ServiceKonto serviceKonto) {
-		return GrpcServiceKonto.newBuilder()
-				.setType(serviceKonto.getType())
-				.addAllPostfachAddresses(getPostfachAddresses(serviceKonto))
-				.setTrustLevel(Optional.ofNullable(serviceKonto.getTrustLevel()).orElse(DEFAULT_TRUST_LEVEL))
-				.build();
-	}
+	@Mapping(target = "postfachAddressesList", source = "postfachAddresses")
+	@Mapping(target = "trustLevel", source = "trustLevel", defaultValue = DEFAULT_TRUST_LEVEL)
+	abstract GrpcServiceKonto toGrpc(ServiceKonto serviceKonto);
 
-	private List<GrpcPostfachAddress> getPostfachAddresses(ServiceKonto serviceKonto) {
-		return serviceKonto.getPostfachAddresses().stream().map(this::fromPostfachAddress).toList();
+	GrpcObject mapFromIdentifier(PostfachAddressIdentifier identifier) {
+		return grpcObjectMapper.fromMap(Map.of(StringBasedIdentifier.POSTFACH_ID_FIELD,
+				getStringBasedValue(identifier)));
 	}
 
-	private GrpcPostfachAddress fromPostfachAddress(PostfachAddress postfachAddress) {
-		return GrpcPostfachAddress.newBuilder()
-				.setVersion(postfachAddress.getVersion())
-				.setType(postfachAddress.getType())
-				.setIdentifier(mapFromIdentifier(postfachAddress.getIdentifier()))
-				.build();
+	String getStringBasedValue(PostfachAddressIdentifier identifier) {
+		return ((StringBasedIdentifier) identifier).getPostfachId();
 	}
 
-	GrpcObject mapFromIdentifier(PostfachAddressIdentifier identifier) {
-		return grpcObjectMapper.fromMap(Map.of(StringBasedIdentifier.POSTFACH_ID_FIELD, getStringBasedValue(identifier)));
+	@Mapping(target = "postfachAddresses", source = "postfachAddressesList")
+	@Mapping(target = "trustLevel", source = "trustLevel", qualifiedByName = "mapTrustLevel")
+	@Mapping(target = "postfachAddress", ignore = true)
+	abstract ServiceKonto fromGrpc(GrpcServiceKonto serviceKonto);
+
+	PostfachAddressIdentifier mapToIdentifier(GrpcObject identifier) {
+		var stringIdentifier = MapUtils.getString(grpcObjectMapper.mapFromGrpc(identifier), StringBasedIdentifier.POSTFACH_ID_FIELD);
+		return StringBasedIdentifier.builder().postfachId(stringIdentifier).build();
 	}
 
-	private String getStringBasedValue(PostfachAddressIdentifier identifier) {
-		return ((StringBasedIdentifier) identifier).getPostfachId();
+	@Named("mapTrustLevel")
+	String mapTrustLevel(String trustLevel) {
+		if (StringUtils.isEmpty(trustLevel)) {
+			return DEFAULT_TRUST_LEVEL;
+		}
+		return trustLevel;
 	}
+
 }
\ No newline at end of file
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcAntragstellerTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcAntragstellerTestFactory.java
new file mode 100644
index 000000000..61893f6a7
--- /dev/null
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcAntragstellerTestFactory.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.router;
+
+import de.ozgcloud.eingang.common.formdata.AntragstellerTestFactory;
+import de.ozgcloud.vorgang.vorgang.GrpcAntragsteller;
+import de.ozgcloud.vorgang.vorgang.GrpcFormData;
+import de.ozgcloud.vorgang.vorgang.GrpcFormField;
+import de.ozgcloud.vorgang.vorgang.GrpcSubForm;
+
+public class GrpcAntragstellerTestFactory {
+
+	public static final String ANREDE = AntragstellerTestFactory.ANREDE;
+	public static final String NACHNAME = AntragstellerTestFactory.NACHNAME;
+	public static final String VORNAME = AntragstellerTestFactory.VORNAME;
+	public static final String GEBURTSDATUM = AntragstellerTestFactory.GEBURTSDATUM;
+	public static final String GEBURTSORT = AntragstellerTestFactory.GEBURTSORT;
+	public static final String GEBURTSNAME = AntragstellerTestFactory.GEBURTSNAME;
+	public static final String EMAIL = AntragstellerTestFactory.EMAIL;
+	public static final String TELEFON = AntragstellerTestFactory.TELEFON;
+	public static final String STRASSE = AntragstellerTestFactory.STRASSE;
+	public static final String HAUSNUMMER = AntragstellerTestFactory.HAUSNUMMER;
+	public static final String PLZ = AntragstellerTestFactory.PLZ;
+	public static final String ORT = AntragstellerTestFactory.ORT;
+
+	public static final String POSTFACH_ID = AntragstellerTestFactory.POSTFACH_ID;
+	public static final GrpcFormData OTHER_DATA = GrpcFormData.newBuilder()
+			.addField(GrpcFormField.newBuilder()
+					.setName(AntragstellerTestFactory.GEBIET_BEZEICHNUNG_KEY)
+					.setValue(AntragstellerTestFactory.GEBIET_BEZEICHNUNG_VALUE).build())
+			.addForm(GrpcSubForm.newBuilder().setTitle(AntragstellerTestFactory.SUB_PERSOENLICHE_EIGNUNG)
+					.addField(GrpcFormField.newBuilder()
+							.setName(AntragstellerTestFactory.SUB_VERBOTENE_VEREINIGUNG_KEY)
+							.setValue(AntragstellerTestFactory.SUB_VERBOTENE_VEREINIGUNG_VALUE).build())
+					.build())
+			.build();
+
+	public static GrpcAntragsteller create() {
+		return createBuilder().build();
+	}
+
+	public static GrpcAntragsteller.Builder createBuilder() {
+		return GrpcAntragsteller.newBuilder()
+				.setAnrede(ANREDE)
+				.setFirmaName(AntragstellerTestFactory.FIRMA_NAME)
+				.setGeburtsdatum(GEBURTSDATUM)
+				.setNachname(NACHNAME)
+				.setVorname(VORNAME)
+				.setGeburtsname(GEBURTSNAME)
+				.setGeburtsort(GEBURTSORT)
+				.setEmail(EMAIL)
+				.setTelefon(TELEFON)
+				.setStrasse(STRASSE)
+				.setHausnummer(HAUSNUMMER)
+				.setPlz(PLZ)
+				.setOrt(ORT)
+				.setPostfachId(POSTFACH_ID)
+				.setOtherData(OTHER_DATA);
+	}
+}
\ No newline at end of file
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangHeaderTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangHeaderTestFactory.java
index e7fbc2a6d..1c2f14b48 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangHeaderTestFactory.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangHeaderTestFactory.java
@@ -30,6 +30,8 @@ import de.ozgcloud.vorgang.vorgang.GrpcServiceKonto;
 
 public class GrpcEingangHeaderTestFactory {
 
+	public static final GrpcServiceKonto SERVICE_KONTO = GrpcServiceKontoTestFactory.create();
+
 	public static GrpcEingangHeader create() {
 		return createBuilder().build();
 	}
@@ -42,7 +44,7 @@ public class GrpcEingangHeaderTestFactory {
 				.setFormEngineName(FORM_ENGINE_NAME)
 				.setFormName(FORM_NAME)
 				.setSender(SENDER)
-				.setServiceKonto(GrpcServiceKonto.newBuilder().build())
+				.setServiceKonto(SERVICE_KONTO)
 				.setVorgangNummer(VORGANG_NUMMER);
 	}
 }
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperTest.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperTest.java
index 15ee7c6b7..a262e8c7a 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperTest.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperTest.java
@@ -40,12 +40,12 @@ import org.mockito.Mock;
 import de.ozgcloud.eingang.common.formdata.AntragstellerTestFactory;
 import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
 import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
+import de.ozgcloud.eingang.common.formdata.ServiceKontoTestFactory;
 import de.ozgcloud.eingang.common.formdata.ZustaendigeStelleTestFactory;
 import de.ozgcloud.vorgang.common.grpc.GrpcFormDataMapper;
 import de.ozgcloud.vorgang.vorgang.GrpcAntragsteller;
 import de.ozgcloud.vorgang.vorgang.GrpcEingang;
 import de.ozgcloud.vorgang.vorgang.GrpcFormData;
-import de.ozgcloud.vorgang.vorgang.GrpcServiceKonto;
 import de.ozgcloud.vorgang.vorgang.GrpcZustaendigeStelle;
 
 class GrpcEingangMapperTest {
@@ -109,7 +109,7 @@ class GrpcEingangMapperTest {
 		void mockMapperReturnValues() {
 			when(grpcFormDataMapper.mapToFormData(anyMap()))
 					.thenReturn(GrpcFormData.newBuilder().addField(GrpcFormFieldTestFactory.create()).build());
-			when(serviceKontoMapper.toServiceKonto(any())).thenReturn(GrpcServiceKonto.newBuilder().build());
+			when(serviceKontoMapper.toGrpc(any())).thenReturn(GrpcServiceKontoTestFactory.create());
 		}
 
 		@Nested
@@ -134,14 +134,14 @@ class GrpcEingangMapperTest {
 			void validateNumberOfAttachments() {
 				var eingang = toEingang();
 
-				assertThat(eingang.getNumberOfAttachments()).isEqualTo(2);
+				assertThat(eingang.getNumberOfAttachments()).isEqualTo(FormDataTestFactory.ATTACHMENTS.size());
 			}
 
 			@Test
 			void validateNumberOfAttachmentGroups() {
 				var eingang = toEingang();
 
-				assertThat(eingang.getAttachmentsCount()).isEqualTo(2);
+				assertThat(eingang.getAttachmentsCount()).isEqualTo(FormDataTestFactory.ATTACHMENTS.size());
 			}
 
 			@Test
@@ -162,25 +162,6 @@ class GrpcEingangMapperTest {
 				assertThat(attachment.getContentType()).isEqualTo(IncomingFileTestFactory.CONTENT_TYPE);
 				assertThat(attachment.getContent()).isEmpty();
 			}
-
-			@Test
-			void validateGroup2AttachmentCount() {
-				var eingang = toEingang();
-
-				assertThat(eingang.getAttachmentsList().get(1).getFilesCount()).isEqualTo(1);
-			}
-
-			@Test
-			void validateGroup2Attachment() {
-				var eingang = toEingang();
-
-				var attachment = eingang.getAttachmentsList().get(1).getFilesList().get(0);
-				assertThat(attachment.getId()).isEqualTo(IncomingFileTestFactory.ID);
-				assertThat(attachment.getVendorId()).isEqualTo(IncomingFileTestFactory.VENDOR_ID);
-				assertThat(attachment.getName()).isEqualTo(IncomingFileTestFactory.NAME);
-				assertThat(attachment.getContentType()).isEqualTo(IncomingFileTestFactory.CONTENT_TYPE);
-				assertThat(attachment.getContent()).isEmpty();
-			}
 		}
 
 		@Nested
@@ -217,4 +198,87 @@ class GrpcEingangMapperTest {
 			return mapper.toEingang(FormDataTestFactory.create(), Optional.of(ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID));
 		}
 	}
+
+	@Nested
+	class TestToFormData {
+
+		private final GrpcEingang eingang = GrpcEingangTestFactory.create();
+
+		@BeforeEach
+		void mock() {
+			when(serviceKontoMapper.fromGrpc(any())).thenReturn(ServiceKontoTestFactory.create());
+			when(grpcFormDataMapper.mapFromFormData(GrpcAntragstellerTestFactory.OTHER_DATA)).thenReturn(AntragstellerTestFactory.DATA);
+			when(grpcFormDataMapper.mapFromFormData(GrpcEingangTestFactory.FORM_DATA)).thenReturn(FormDataTestFactory.FORM_DATA);
+		}
+
+		@Test
+		void shouldCallServiceKontoMapper() {
+			mapper.toFormData(eingang);
+
+			verify(serviceKontoMapper).fromGrpc(GrpcEingangHeaderTestFactory.SERVICE_KONTO);
+		}
+
+		@Test
+		void shouldCallFormDataMapperWithAntragstellerData() {
+			mapper.toFormData(eingang);
+
+			verify(grpcFormDataMapper).mapFromFormData(GrpcAntragstellerTestFactory.OTHER_DATA);
+		}
+
+		@Test
+		void shouldCallFormDataMapperWithFormData() {
+			mapper.toFormData(eingang);
+
+			verify(grpcFormDataMapper).mapFromFormData(GrpcEingangTestFactory.FORM_DATA);
+		}
+
+		@Test
+		void shouldMapToFormData() {
+			var expectedFormData = FormDataTestFactory.createBuilder()
+					.numberOfAttachments(GrpcEingangTestFactory.NUMBER_OF_ATTACHMENTS)
+					.numberOfRepresentations(GrpcEingangTestFactory.NUMBER_OF_REPRESENTATIONS)
+					.build();
+
+			var formData = mapper.toFormData(eingang);
+
+			assertThat(formData).usingRecursiveComparison()
+					.ignoringFields("antragsteller.firmaName", "control", "attachments", "representations", "zustaendigeStelles")
+					.isEqualTo(expectedFormData);
+		}
+
+		@Nested
+		class TestZustaendigestelles {
+
+			@Test
+			void shouldMapZustaendigeStelles() {
+				var zustaendigeStelles = mapper.toFormData(eingang).getZustaendigeStelles();
+
+				assertThat(zustaendigeStelles).usingRecursiveFieldByFieldElementComparator().containsExactly(ZustaendigeStelleTestFactory.create());
+			}
+		}
+
+		@Nested
+		class TestAttachments {
+
+			@Test
+			void shouldMapAttachments() {
+				var attachments = mapper.toFormData(eingang).getAttachments();
+
+				assertThat(attachments).usingRecursiveFieldByFieldElementComparatorIgnoringFields("files.file")
+						.containsExactly(FormDataTestFactory.ATTACHMENTS.get(0));
+			}
+		}
+
+		@Nested
+		class TestRepresentations {
+
+			@Test
+			void shouldMapRepresentations() {
+				var representations = mapper.toFormData(eingang).getRepresentations();
+
+				assertThat(representations).usingRecursiveFieldByFieldElementComparatorIgnoringFields("file")
+						.containsExactly(FormDataTestFactory.REPRESENTATIONS.get(0));
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangTestFactory.java
new file mode 100644
index 000000000..8e15b541a
--- /dev/null
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangTestFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.router;
+
+import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
+import de.ozgcloud.vorgang.common.grpc.GrpcFormDataTestFactory;
+import de.ozgcloud.vorgang.common.grpc.GrpcSubFormTestFactory;
+import de.ozgcloud.vorgang.vorgang.GrpcEingang;
+import de.ozgcloud.vorgang.vorgang.GrpcFormData;
+import de.ozgcloud.vorgang.vorgang.GrpcSubForm;
+
+public class GrpcEingangTestFactory {
+
+	public static final GrpcFormData FORM_DATA = GrpcFormDataTestFactory.create();
+	public static final int NUMBER_OF_REPRESENTATIONS = 5;
+	public static final int NUMBER_OF_ATTACHMENTS = 4;
+	public static final String ID = FormDataTestFactory.ID;
+	public static final GrpcSubForm SUB_FORM = GrpcSubFormTestFactory.create();
+
+	public static GrpcEingang create() {
+		return createBuilder().build();
+	}
+
+	public static GrpcEingang.Builder createBuilder() {
+		return GrpcEingang.newBuilder()
+				.setId(ID)
+				.setHeader(GrpcEingangHeaderTestFactory.create())
+				.setAntragsteller(GrpcAntragstellerTestFactory.create())
+				.setZustaendigeStelle(GrpcZustaendigeStelleTestFactory.create())
+				.setFormData(FORM_DATA)
+				.addAttachments(GrpcIncomingFileGroupTestFactory.create())
+				.setNumberOfAttachments(NUMBER_OF_ATTACHMENTS)
+				.addRepresentations(GrpcIncomingFileTestFactory.create())
+				.setNumberOfRepresentations(NUMBER_OF_REPRESENTATIONS);
+	}
+}
\ No newline at end of file
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcIncomingFileGroupTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcIncomingFileGroupTestFactory.java
index 8ffea9796..e8082ab91 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcIncomingFileGroupTestFactory.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcIncomingFileGroupTestFactory.java
@@ -23,12 +23,13 @@
  */
 package de.ozgcloud.eingang.router;
 
+import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory;
 import de.ozgcloud.vorgang.vorgang.GrpcIncomingFile;
 import de.ozgcloud.vorgang.vorgang.GrpcIncomingFileGroup;
 
 public class GrpcIncomingFileGroupTestFactory {
 
-	public static final String NAME = "FileGroup1";
+	public static final String NAME = IncomingFileGroupTestFactory.NAME;
 	public static final GrpcIncomingFile FILE = GrpcIncomingFileTestFactory.create();
 
 	public static GrpcIncomingFileGroup create() {
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcIncomingFileTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcIncomingFileTestFactory.java
index dc5899c75..f72d588d6 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcIncomingFileTestFactory.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcIncomingFileTestFactory.java
@@ -30,7 +30,7 @@ import de.ozgcloud.vorgang.vorgang.GrpcIncomingFile;
 
 public class GrpcIncomingFileTestFactory {
 
-	public static final String ID = IncomingFileTestFactory.ID.toString();
+	public static final String ID = IncomingFileTestFactory.ID;
 	public static final String VENDOR_ID = IncomingFileTestFactory.VENDOR_ID;
 	public static final String NAME = IncomingFileTestFactory.NAME;
 	public static final String CONTENT_TYPE = IncomingFileTestFactory.CONTENT_TYPE;
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcPostfachAddressTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcPostfachAddressTestFactory.java
new file mode 100644
index 000000000..f8ba6cd4b
--- /dev/null
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcPostfachAddressTestFactory.java
@@ -0,0 +1,33 @@
+package de.ozgcloud.eingang.router;
+
+import de.ozgcloud.eingang.common.formdata.PostfachAddressTestFactory;
+import de.ozgcloud.eingang.common.formdata.StringBasedIdentifier;
+import de.ozgcloud.vorgang.common.GrpcObject;
+import de.ozgcloud.vorgang.common.GrpcProperty;
+import de.ozgcloud.vorgang.vorgang.GrpcPostfachAddress;
+import de.ozgcloud.vorgang.vorgang.GrpcPostfachAddress.Builder;
+
+public class GrpcPostfachAddressTestFactory {
+
+	public static final String VERSION = PostfachAddressTestFactory.VERSION;
+	public static final int TYPE = PostfachAddressTestFactory.POSTFACH_ADDRESS_TYPE;
+	public static final GrpcProperty IDENTIFIER_PROPERTY = GrpcProperty.newBuilder()
+			.setName(StringBasedIdentifier.POSTFACH_ID_FIELD)
+			.addValue(PostfachAddressTestFactory.POSTFACH_ID)
+			.build();
+	public static final GrpcObject IDENTIFIER = GrpcObject.newBuilder()
+			.addProperty(IDENTIFIER_PROPERTY)
+			.build();
+
+	public static GrpcPostfachAddress create() {
+		return createBuilder().build();
+	}
+
+	private static Builder createBuilder() {
+		return GrpcPostfachAddress.newBuilder()
+				.setVersion(VERSION)
+				.setType(TYPE)
+				.setIdentifier(IDENTIFIER);
+	}
+
+}
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcServiceKontoTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcServiceKontoTestFactory.java
new file mode 100644
index 000000000..1995e8123
--- /dev/null
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcServiceKontoTestFactory.java
@@ -0,0 +1,25 @@
+package de.ozgcloud.eingang.router;
+
+import de.ozgcloud.eingang.common.formdata.ServiceKontoTestFactory;
+import de.ozgcloud.vorgang.vorgang.GrpcPostfachAddress;
+import de.ozgcloud.vorgang.vorgang.GrpcServiceKonto;
+import de.ozgcloud.vorgang.vorgang.GrpcServiceKonto.Builder;
+
+public class GrpcServiceKontoTestFactory {
+
+	public static final GrpcPostfachAddress POSTFACH_ADDRESS = GrpcPostfachAddressTestFactory.create();
+	public static final String TRUST_LEVEL = ServiceKontoTestFactory.TRUST_LEVEL;
+	public static final String TYPE = ServiceKontoTestFactory.TYPE;
+
+	public static GrpcServiceKonto create() {
+		return createBuilder().build();
+	}
+
+	public static Builder createBuilder() {
+		return GrpcServiceKonto.newBuilder()
+				.setType(TYPE)
+				.setTrustLevel(TRUST_LEVEL)
+				.addPostfachAddresses(POSTFACH_ADDRESS);
+	}
+
+}
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcZustaendigeStelleTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcZustaendigeStelleTestFactory.java
new file mode 100644
index 000000000..d96403837
--- /dev/null
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcZustaendigeStelleTestFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.router;
+
+import de.ozgcloud.eingang.common.formdata.ZustaendigeStelleTestFactory;
+import de.ozgcloud.vorgang.vorgang.GrpcZustaendigeStelle;
+
+public class GrpcZustaendigeStelleTestFactory {
+
+	public static final String ORGANISATIONSEINHEIT_ID = ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID;
+	public static final String EMAIL = ZustaendigeStelleTestFactory.EMAIL;
+	public static final String GEMEINDE_SCHLUESSEL = ZustaendigeStelleTestFactory.GEMEINDE_SCHLUESSEL;
+	public static final String AMTLICHER_REGIONAL_SCHLUESSEL = ZustaendigeStelleTestFactory.AMTLICHER_REGIONAL_SCHLUESSEL;
+	public static final String HAUSANSCHRIFT_STRASSE = ZustaendigeStelleTestFactory.HAUSANSCHRIFT_STRASSE;
+	public static final String HAUSANSCHRIFT_PLZ = ZustaendigeStelleTestFactory.HAUSANSCHRIFT_PLZ;
+	public static final String HAUSANSCHRIFT_ORT = ZustaendigeStelleTestFactory.HAUSANSCHRIFT_ORT;
+	public static final String TELEFON = ZustaendigeStelleTestFactory.TELEFON;
+	public static final String BEZEICHNUNG = ZustaendigeStelleTestFactory.BEZEICHNUNG;
+
+	public static GrpcZustaendigeStelle create() {
+		return createBuilder().build();
+	}
+
+	public static GrpcZustaendigeStelle.Builder createBuilder() {
+		return GrpcZustaendigeStelle.newBuilder()
+				.setOrganisationseinheitenId(ORGANISATIONSEINHEIT_ID)
+				.setEmail(EMAIL)
+				.setBezeichnung(BEZEICHNUNG)
+				.setGemeindeSchluessel(GEMEINDE_SCHLUESSEL)
+				.setAmtlicherRegionalSchluessel(AMTLICHER_REGIONAL_SCHLUESSEL)
+				.setHausanschriftStrasse(HAUSANSCHRIFT_STRASSE)
+				.setHausanschriftPlz(HAUSANSCHRIFT_PLZ)
+				.setHausanschriftOrt(HAUSANSCHRIFT_ORT)
+				.setTelefon(TELEFON);
+	}
+}
\ No newline at end of file
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/ServiceKontoMapperTest.java b/router/src/test/java/de/ozgcloud/eingang/router/ServiceKontoMapperTest.java
index 34cc0776f..f6389906f 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/ServiceKontoMapperTest.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/ServiceKontoMapperTest.java
@@ -27,10 +27,14 @@ import static org.assertj.core.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
 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.mapstruct.factory.Mappers;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 
@@ -45,13 +49,12 @@ import de.ozgcloud.vorgang.vorgang.GrpcServiceKonto;
 class ServiceKontoMapperTest {
 
 	@InjectMocks
-	private ServiceKontoMapper mapper;
+	private final ServiceKontoMapper mapper = Mappers.getMapper(ServiceKontoMapper.class);
 	@Mock
 	private GrpcObjectMapper grpcObjectMapper;
 
 	@Nested
-	@DisplayName("To servicekonto")
-	class TestMapServiceKonto {
+	class TestToGrpc {
 
 		private final GrpcProperty grpcProperty = GrpcProperty.newBuilder()
 				.setName(StringBasedIdentifier.POSTFACH_ID_FIELD)
@@ -89,7 +92,7 @@ class ServiceKontoMapperTest {
 
 		@Test
 		void shouldContainsDefaultTrustLevel() {
-			var serviceKonto = mapper.toServiceKonto(ServiceKontoTestFactory.createBuilder().trustLevel(null).build());
+			var serviceKonto = mapper.toGrpc(ServiceKontoTestFactory.createBuilder().trustLevel(null).build());
 
 			assertThat(serviceKonto.getTrustLevel()).isEqualTo(ServiceKontoMapper.DEFAULT_TRUST_LEVEL);
 		}
@@ -139,7 +142,38 @@ class ServiceKontoMapperTest {
 		}
 
 		private GrpcServiceKonto getServiceKontoFromMappedEingang() {
-			return mapper.toServiceKonto(ServiceKontoTestFactory.create());
+			return mapper.toGrpc(ServiceKontoTestFactory.create());
+		}
+	}
+
+	@Nested
+	class TestFromGrpc {
+
+		@BeforeEach
+		void mockGrpcObjectMapper() {
+			when(grpcObjectMapper.mapFromGrpc(any()))
+					.thenReturn(Map.of(StringBasedIdentifier.POSTFACH_ID_FIELD, PostfachAddressTestFactory.POSTFACH_ID));
+		}
+
+		@Test
+		void shouldCallGrpcMapper() {
+			mapper.fromGrpc(GrpcServiceKontoTestFactory.create());
+
+			verify(grpcObjectMapper).mapFromGrpc(GrpcPostfachAddressTestFactory.IDENTIFIER);
+		}
+
+		@Test
+		void shouldMapToGrpc() {
+			var serviceKonto = mapper.fromGrpc(GrpcServiceKontoTestFactory.create());
+
+			assertThat(serviceKonto).usingRecursiveComparison().isEqualTo(ServiceKontoTestFactory.create());
+		}
+
+		@Test
+		void shouldSetDefaultTrustLevel() {
+			var serviceKonto = mapper.fromGrpc(GrpcServiceKontoTestFactory.createBuilder().setTrustLevel(StringUtils.EMPTY).build());
+
+			assertThat(serviceKonto.getTrustLevel()).isEqualTo(ServiceKontoMapper.DEFAULT_TRUST_LEVEL);
 		}
 	}
 }
\ No newline at end of file
-- 
GitLab


From 7dfb7f91b04233409c442a65ec801a46ca0a4302 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Wed, 12 Mar 2025 14:47:41 +0100
Subject: [PATCH 02/28] OZG-7573 OZG-7929 forwarding call VorgangService

---
 forwarder/pom.xml                             |  6 ++
 .../eingang/forwarder/RouteCriteria.java      |  2 +-
 .../forwarder/RouteCriteriaMapper.java        |  2 +
 .../forwarder/RouteForwardingGrpcService.java | 13 ++--
 .../forwarder/RouteForwardingService.java     | 14 +++-
 .../forwarder/RouteCriteriaTestFactory.java   |  6 +-
 .../forwarder/RouteForwardingServiceTest.java | 66 +++++++++++++++++++
 7 files changed, 96 insertions(+), 13 deletions(-)
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java

diff --git a/forwarder/pom.xml b/forwarder/pom.xml
index 8e11aac68..c09443335 100644
--- a/forwarder/pom.xml
+++ b/forwarder/pom.xml
@@ -86,6 +86,12 @@
 			<artifactId>lombok</artifactId>
 		</dependency>
 
+		<!-- Test -->
+		<dependency>
+			<groupId>de.ozgcloud.eingang</groupId>
+			<artifactId>common</artifactId>
+			<type>test-jar</type>
+		</dependency>
 	</dependencies>
 
 	<build>
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java
index b52e416dd..3a36486c9 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java
@@ -34,5 +34,5 @@ public class RouteCriteria {
 
 	private Optional<String> gemeindeSchluessel;
 	private Optional<String> webserviceUrl;
-	private Optional<String> organisationseinheitenId;
+	private Optional<String> organisationEinheitId;
 }
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteriaMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteriaMapper.java
index d82fbd76b..7fcb6cea6 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteriaMapper.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteriaMapper.java
@@ -27,12 +27,14 @@ import java.util.Optional;
 
 import org.apache.commons.lang3.StringUtils;
 import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
 
 import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
 
 @Mapper
 interface RouteCriteriaMapper {
 
+	@Mapping(target = "organisationEinheitId", source = "organisationseinheitenId")
 	RouteCriteria fromGrpc(GrpcRouteCriteria grpcRouteCriteria);
 
 	default Optional<String> wrapWithOptional(String val) {
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
index fa9c9d99a..6827f8839 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
@@ -23,24 +23,21 @@
  */
 package de.ozgcloud.eingang.forwarder;
 
-import org.springframework.beans.factory.annotation.Autowired;
-
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingResponse;
 import de.ozgcloud.eingang.router.GrpcEingangMapper;
 import io.grpc.stub.StreamObserver;
+import lombok.RequiredArgsConstructor;
 import net.devh.boot.grpc.server.service.GrpcService;
 
 @GrpcService
+@RequiredArgsConstructor
 public class RouteForwardingGrpcService extends RouteForwardingServiceGrpc.RouteForwardingServiceImplBase {
 
-	@Autowired
-	private RouteForwardingService service;
-	@Autowired
-	private RouteCriteriaMapper criteriaMapper;
+	private final RouteForwardingService service;
+	private final RouteCriteriaMapper criteriaMapper;
 
-	@Autowired
-	private GrpcEingangMapper eingangMapper;
+	private final GrpcEingangMapper eingangMapper;
 
 	@Override
 	public void routeForwarding(GrpcRouteForwardingRequest request, StreamObserver<GrpcRouteForwardingResponse> responseObserver) {
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
index 0079be1be..8df1ca5ae 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
@@ -26,11 +26,23 @@ package de.ozgcloud.eingang.forwarder;
 import org.springframework.stereotype.Service;
 
 import de.ozgcloud.eingang.common.formdata.FormData;
+import de.ozgcloud.eingang.common.formdata.ZustaendigeStelle;
+import de.ozgcloud.eingang.router.VorgangService;
+import lombok.RequiredArgsConstructor;
 
 @Service
+@RequiredArgsConstructor
 class RouteForwardingService {
 
+	private final VorgangService vorgangService;
+
 	public void route(RouteCriteria criteria, FormData formData) {
-//FIXME implement me
+		vorgangService.createVorgang(formData.toBuilder()
+				.clearZustaendigeStelles()
+				.zustaendigeStelle(ZustaendigeStelle.builder()
+						.organisationseinheitenId(criteria.getOrganisationEinheitId()
+								.orElseThrow(() -> new UnsupportedOperationException("OrganisationseinheitId is required!")))
+						.build())
+				.build());
 	}
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java
index f5b8c2742..82a9dc490 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java
@@ -31,7 +31,7 @@ public class RouteCriteriaTestFactory {
 
 	public static final String GEMEINDE_SCHLUSSEL = "0815";
 	public static final String WEBSERVICE_URL = "http://nimmerland.by.kop-cloud.de/ws";
-	public static final String ORGANISATIONSEINHEITEN_ID = "4711";
+	public static final String ORGANISATION_EINHEIT_ID = "4711";
 
 	public static RouteCriteria create() {
 		return createBuilder().build();
@@ -41,7 +41,7 @@ public class RouteCriteriaTestFactory {
 		return RouteCriteria.builder()
 				.gemeindeSchluessel(Optional.of(GEMEINDE_SCHLUSSEL))
 				.webserviceUrl(Optional.of(WEBSERVICE_URL))
-				.organisationseinheitenId(Optional.of(ORGANISATIONSEINHEITEN_ID));
+				.organisationEinheitId(Optional.of(ORGANISATION_EINHEIT_ID));
 	}
 
 	public static GrpcRouteCriteria createGrpc() {
@@ -52,6 +52,6 @@ public class RouteCriteriaTestFactory {
 		return GrpcRouteCriteria.newBuilder()
 				.setGemeindeSchluessel(GEMEINDE_SCHLUSSEL)
 				.setWebserviceUrl(WEBSERVICE_URL)
-				.setOrganisationseinheitenId(ORGANISATIONSEINHEITEN_ID);
+				.setOrganisationseinheitenId(ORGANISATION_EINHEIT_ID);
 	}
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
new file mode 100644
index 000000000..e4c711fbc
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
@@ -0,0 +1,66 @@
+package de.ozgcloud.eingang.forwarder;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import de.ozgcloud.eingang.common.formdata.FormData;
+import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
+import de.ozgcloud.eingang.common.formdata.ZustaendigeStelle;
+import de.ozgcloud.eingang.router.VorgangService;
+
+class RouteForwardingServiceTest {
+
+	@InjectMocks
+	private RouteForwardingService routeForwardingService;
+	@Mock
+	private VorgangService vorgangService;
+	@Captor
+	private ArgumentCaptor<FormData> formDataCaptor;
+
+	@Nested
+	class TestRoute {
+
+		private final FormData formData = FormDataTestFactory.create();
+
+		@Test
+		void shouldCallVorgangService() {
+			var criteria = RouteCriteriaTestFactory.create();
+
+			routeForwardingService.route(criteria, formData);
+
+			verify(vorgangService).createVorgang(formDataCaptor.capture());
+			assertThat(formDataCaptor.getValue()).usingRecursiveComparison().ignoringFields("zustaendigeStelles").isEqualTo(formData);
+		}
+
+		@Test
+		void shouldSetOrganisationEinheitIdInFormData() {
+			var criteria = RouteCriteriaTestFactory.create();
+			var expectedZustaendigeStelle = ZustaendigeStelle.builder()
+					.organisationseinheitenId(RouteCriteriaTestFactory.ORGANISATION_EINHEIT_ID)
+					.build();
+
+			routeForwardingService.route(criteria, formData);
+
+			verify(vorgangService).createVorgang(formDataCaptor.capture());
+			assertThat(formDataCaptor.getValue().getZustaendigeStelles()).usingRecursiveFieldByFieldElementComparator()
+					.containsExactly(expectedZustaendigeStelle);
+		}
+
+		@Test
+		void shouldThrowUnsupportedOperationException() {
+			var criteria = RouteCriteriaTestFactory.createBuilder().organisationEinheitId(Optional.empty()).build();
+
+			assertThrows(UnsupportedOperationException.class, () -> routeForwardingService.route(criteria, formData));
+		}
+	}
+}
-- 
GitLab


From 5be6f31452c5362b4d2ee84e1c287f1ddc9d5adb Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Wed, 12 Mar 2025 14:54:41 +0100
Subject: [PATCH 03/28] OZG-7573 fix unit test after test factory chagens

---
 .../de/ozgcloud/eingang/router/VorgangRemoteServiceTest.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/router/src/test/java/de/ozgcloud/eingang/router/VorgangRemoteServiceTest.java b/router/src/test/java/de/ozgcloud/eingang/router/VorgangRemoteServiceTest.java
index 7fda033b4..cc8cef24b 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/VorgangRemoteServiceTest.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/VorgangRemoteServiceTest.java
@@ -342,7 +342,7 @@ class VorgangRemoteServiceTest {
 			void shouldCallUploadIncomingFile() {
 				vorgangCreator.uploadAttachments();
 
-				verify(vorgangCreator, times(2)).uploadIncomingFile(any(IncomingFile.class));
+				verify(vorgangCreator, times(FormDataTestFactory.ATTACHMENTS.size())).uploadIncomingFile(any(IncomingFile.class));
 			}
 
 			@Test
-- 
GitLab


From c969cf801e006138d391bb0414218b46aa6842fd Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Wed, 12 Mar 2025 17:26:39 +0100
Subject: [PATCH 04/28] OZG-7573 more test fixing after test factory change

---
 forwarder/pom.xml                             | 13 ++++++
 ...GrpcRouteForwardingRequestTestFactory.java |  9 +++-
 .../RouteForwardingGrpcServiceTest.java       | 41 +++++++++---------
 pom.xml                                       |  7 ++++
 router/pom.xml                                | 18 ++++++++
 .../router/GrpcEingangMapperITCase.java       | 42 ++++++++++++-------
 .../eingang/router/VorgangServiceTest.java    |  4 +-
 7 files changed, 96 insertions(+), 38 deletions(-)

diff --git a/forwarder/pom.xml b/forwarder/pom.xml
index c09443335..d79e4343e 100644
--- a/forwarder/pom.xml
+++ b/forwarder/pom.xml
@@ -91,6 +91,19 @@
 			<groupId>de.ozgcloud.eingang</groupId>
 			<artifactId>common</artifactId>
 			<type>test-jar</type>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>de.ozgcloud.eingang</groupId>
+			<artifactId>router</artifactId>
+			<type>test-jar</type>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>de.ozgcloud.vorgang</groupId>
+			<artifactId>vorgang-manager-utils</artifactId>
+			<type>test-jar</type>
+			<scope>test</scope>
 		</dependency>
 	</dependencies>
 
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
index 03c5beaed..37f758e1d 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
@@ -23,16 +23,23 @@
  */
 package de.ozgcloud.eingang.forwarder;
 
+import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest;
+import de.ozgcloud.eingang.router.GrpcEingangTestFactory;
+import de.ozgcloud.vorgang.vorgang.GrpcEingang;
 
 public class GrpcRouteForwardingRequestTestFactory {
 
+	public static final GrpcEingang EINGANG = GrpcEingangTestFactory.create();
+	public static final GrpcRouteCriteria CRITERIA = RouteCriteriaTestFactory.createGrpc();
+
 	public static GrpcRouteForwardingRequest create() {
 		return createBuilder().build();
 	}
 
 	public static GrpcRouteForwardingRequest.Builder createBuilder() {
 		return GrpcRouteForwardingRequest.newBuilder()
-				.setRouteCriteria(RouteCriteriaTestFactory.createGrpc());
+				.setEingang(EINGANG)
+				.setRouteCriteria(CRITERIA);
 	}
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
index e4dd78ddb..e165091ee 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
@@ -23,19 +23,17 @@
  */
 package de.ozgcloud.eingang.forwarder;
 
-import static org.assertj.core.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
-import org.mapstruct.factory.Mappers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
-import org.mockito.Spy;
 
+import de.ozgcloud.eingang.common.formdata.FormData;
+import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingResponse;
 import de.ozgcloud.eingang.router.GrpcEingangMapper;
 import io.grpc.stub.StreamObserver;
@@ -50,39 +48,42 @@ class RouteForwardingGrpcServiceTest {
 
 	@Mock
 	private RouteForwardingService routeService;
-	@Spy
-	private RouteCriteriaMapper criteriaMapper = Mappers.getMapper(RouteCriteriaMapper.class);
-	@Spy
-	private GrpcEingangMapper eingangMapper = Mappers.getMapper(GrpcEingangMapper.class);
+	@Mock
+	private RouteCriteriaMapper criteriaMapper;
+	@Mock
+	private GrpcEingangMapper eingangMapper;
 
 	@Nested
 	class TestRouteForwarding {
 
-		@Captor
-		private ArgumentCaptor<RouteCriteria> criteriaCaptor;
+		private final RouteCriteria routeCriteria = RouteCriteriaTestFactory.create();
+		private final FormData formData = FormDataTestFactory.create();
+
+		@BeforeEach
+		void mock() {
+			when(criteriaMapper.fromGrpc(any())).thenReturn(routeCriteria);
+			when(eingangMapper.toFormData(any())).thenReturn(formData);
+		}
 
 		@Test
-		void shouldCallService() {
+		void shouldMapCriteria() {
 			service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(), responseObserver);
 
-			verify(routeService).route(any(), any());
+			verify(criteriaMapper).fromGrpc(GrpcRouteForwardingRequestTestFactory.CRITERIA);
 		}
 
 		@Test
-		void shouldHaveRouteCriteria() {
+		void shouldMapEingang() {
 			service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(), responseObserver);
 
-			verify(routeService).route(criteriaCaptor.capture(), any());
-
-			assertThat(criteriaCaptor.getValue()).usingRecursiveComparison().isEqualTo(RouteCriteriaTestFactory.create());
+			verify(eingangMapper).toFormData(GrpcRouteForwardingRequestTestFactory.EINGANG);
 		}
 
 		@Test
-		void shouldHaveFormData() {
+		void shouldCallService() {
 			service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(), responseObserver);
 
-			verify(routeService).route(any(), notNull());
-			verify(eingangMapper).toFormData(any());
+			verify(routeService).route(routeCriteria, formData);
 		}
 	}
 
diff --git a/pom.xml b/pom.xml
index ce7c182f5..4eb2c2578 100644
--- a/pom.xml
+++ b/pom.xml
@@ -126,6 +126,13 @@
 				<scope>test</scope>
 				<version>${project.version}</version>
 			</dependency>
+			<dependency>
+				<groupId>de.ozgcloud.eingang</groupId>
+				<artifactId>router</artifactId>
+				<version>${project.version}</version>
+				<type>test-jar</type>
+				<scope>test</scope>
+			</dependency>
 			<dependency>
 				<groupId>de.ozgcloud.vorgang</groupId>
 				<artifactId>vorgang-manager-utils</artifactId>
diff --git a/router/pom.xml b/router/pom.xml
index 7130d2c1e..5964dfb91 100644
--- a/router/pom.xml
+++ b/router/pom.xml
@@ -92,10 +92,28 @@
 			<type>test-jar</type>
 			<scope>test</scope>
 		</dependency>
+		<dependency>
+			<groupId>de.ozgcloud.vorgang</groupId>
+			<artifactId>vorgang-manager-utils</artifactId>
+			<type>test-jar</type>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
 
 	<build>
 		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<executions>
+					<execution>
+						<goals>
+							<goal>test-jar</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+
 			<plugin>
 				<groupId>org.jacoco</groupId>
 				<artifactId>jacoco-maven-plugin</artifactId>
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperITCase.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperITCase.java
index 55e1b3ef3..758fcd0ef 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperITCase.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperITCase.java
@@ -29,7 +29,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -38,6 +37,7 @@ import org.springframework.boot.test.context.SpringBootTest;
 
 import de.ozgcloud.eingang.common.formdata.AntragstellerTestFactory;
 import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
+import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory;
 import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
 import de.ozgcloud.eingang.common.formdata.ZustaendigeStelleTestFactory;
 import de.ozgcloud.vorgang.vorgang.GrpcEingang;
@@ -59,7 +59,9 @@ class GrpcEingangMapperITCase {
 
 			@Test
 			void antragstellerShouldBeMapped() {
-				var antragSteller = grpcEingangMapper.toEingang(FormDataTestFactory.create(), Optional.of(ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID)).getAntragsteller();
+				var antragSteller = grpcEingangMapper
+						.toEingang(FormDataTestFactory.create(), Optional.of(ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID))
+						.getAntragsteller();
 
 				assertThat(antragSteller.getPostfachId()).isEqualTo(AntragstellerTestFactory.POSTFACH_ID);
 				assertThat(antragSteller.getVorname()).isEqualTo(AntragstellerTestFactory.VORNAME);
@@ -68,7 +70,9 @@ class GrpcEingangMapperITCase {
 
 			@Test
 			void dataShouldBeMapped() {
-				var antragsteller = grpcEingangMapper.toEingang(FormDataTestFactory.create(), Optional.of(ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID)).getAntragsteller();
+				var antragsteller = grpcEingangMapper
+						.toEingang(FormDataTestFactory.create(), Optional.of(ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID))
+						.getAntragsteller();
 
 				assertThat(antragsteller.getOtherData().getFieldList()).hasSize(1);
 				assertThat(antragsteller.getOtherData().getField(0).getName()).isEqualTo(AntragstellerTestFactory.GEBIET_BEZEICHNUNG_KEY);
@@ -81,7 +85,9 @@ class GrpcEingangMapperITCase {
 		class TestZustaendigeStelle {
 			@Test
 			void eingangShouldHaveZustaendigeStelle() {
-				var zustaendigeStelle = grpcEingangMapper.toEingang(FormDataTestFactory.create(), Optional.of(ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID)).getZustaendigeStelle();
+				var zustaendigeStelle = grpcEingangMapper
+						.toEingang(FormDataTestFactory.create(), Optional.of(ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID))
+						.getZustaendigeStelle();
 
 				assertThat(zustaendigeStelle).isNotNull();
 				assertThat(zustaendigeStelle.getOrganisationseinheitenId()).isEqualTo(ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID);
@@ -102,34 +108,30 @@ class GrpcEingangMapperITCase {
 		@DisplayName("Test mapped Attachments")
 		class TestAttachments {
 
-			private GrpcEingang eingang;
-
-			@BeforeEach
-			void init() {
-
-				eingang = grpcEingangMapper.toEingang(FormDataTestFactory.create(), Optional.empty());
-			}
-
 			@Test
 			void validateNumberOfAttachments() {
+				var eingang = grpcEingangMapper.toEingang(FormDataTestFactory.create(), Optional.empty());
 
-				assertThat(eingang.getNumberOfAttachments()).isEqualTo(2);
+				assertThat(eingang.getNumberOfAttachments()).isEqualTo(FormDataTestFactory.ATTACHMENTS.size());
 			}
 
 			@Test
 			void validateNumberOfAttachmentGroups() {
+				var eingang = grpcEingangMapper.toEingang(FormDataTestFactory.create(), Optional.empty());
 
-				assertThat(eingang.getAttachmentsCount()).isEqualTo(2);
+				assertThat(eingang.getAttachmentsCount()).isEqualTo(FormDataTestFactory.ATTACHMENTS.size());
 			}
 
 			@Test
 			void validateGroup1AttachmentCount() {
+				var eingang = grpcEingangMapper.toEingang(FormDataTestFactory.create(), Optional.empty());
 
 				assertThat(eingang.getAttachmentsList().get(0).getFilesCount()).isEqualTo(1);
 			}
 
 			@Test
 			void validateGroup1Attachment() {
+				var eingang = grpcEingangMapper.toEingang(FormDataTestFactory.create(), Optional.empty());
 
 				GrpcIncomingFile attachment = eingang.getAttachmentsList().get(0).getFilesList().get(0);
 
@@ -142,12 +144,18 @@ class GrpcEingangMapperITCase {
 
 			@Test
 			void validateGroup2AttachmentCount() {
+				var attachmentGroup2 = IncomingFileGroupTestFactory.createBuilder().name("attachmentGroup2").build();
+
+				var eingang = grpcEingangMapper.toEingang(FormDataTestFactory.createBuilder().attachment(attachmentGroup2).build(), Optional.empty());
 
 				assertThat(eingang.getAttachmentsList().get(1).getFilesCount()).isEqualTo(1);
 			}
 
 			@Test
 			void validateGroup2Attachment() {
+				var attachmentGroup2 = IncomingFileGroupTestFactory.createBuilder().name("attachmentGroup2").build();
+
+				var eingang = grpcEingangMapper.toEingang(FormDataTestFactory.createBuilder().attachment(attachmentGroup2).build(), Optional.empty());
 
 				GrpcIncomingFile attachment = eingang.getAttachmentsList().get(1).getFilesList().get(0);
 
@@ -187,7 +195,8 @@ class GrpcEingangMapperITCase {
 			void valueListShouldGenerateFields() {
 
 				GrpcEingang eingang = grpcEingangMapper
-						.toEingang(FormDataTestFactory.createBuilder().formData(Map.of("key", List.of("value1", "value2"))).build(), Optional.empty());
+						.toEingang(FormDataTestFactory.createBuilder().formData(Map.of("key", List.of("value1", "value2"))).build(),
+								Optional.empty());
 
 				assertThat(eingang.getFormData().getFieldCount()).isEqualTo(2);
 			}
@@ -197,7 +206,8 @@ class GrpcEingangMapperITCase {
 
 				GrpcEingang eingang = grpcEingangMapper
 						.toEingang(FormDataTestFactory.createBuilder()
-								.formData(Map.of("key-1", List.of(Map.of("sub_key", "value1"), Map.of("sub_key", "value2")))).build(), Optional.empty());
+								.formData(Map.of("key-1", List.of(Map.of("sub_key", "value1"), Map.of("sub_key", "value2")))).build(),
+								Optional.empty());
 
 				assertThat(eingang.getFormData().getFormCount()).isEqualTo(2);
 				assertThat(eingang.getFormData().getForm(0).getFieldCount()).isEqualTo(1);
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/VorgangServiceTest.java b/router/src/test/java/de/ozgcloud/eingang/router/VorgangServiceTest.java
index d69b36c66..bd5ff001a 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/VorgangServiceTest.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/VorgangServiceTest.java
@@ -27,6 +27,8 @@ import static de.ozgcloud.eingang.common.formdata.FormDataTestFactory.*;
 import static java.util.Collections.*;
 import static org.assertj.core.api.Assertions.*;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.*;
 
 import java.io.IOException;
@@ -152,7 +154,7 @@ class VorgangServiceTest {
 		void shouldReturnAttachmentsAndRepresentations() {
 			var files = service.getFormDataFiles(formData).toList();
 
-			assertThat(files).hasSize(3);
+			assertThat(files).hasSize(FormDataTestFactory.ATTACHMENTS.size() + FormDataTestFactory.REPRESENTATIONS.size());
 		}
 	}
 
-- 
GitLab


From cea513cccab8a1ee19cd7d292454869220916120 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Wed, 12 Mar 2025 17:32:41 +0100
Subject: [PATCH 05/28] OZG-7573 fix test after test factory change

---
 .../ozgcloud/eingang/semantik/common/FormDataTestFactory.java  | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/FormDataTestFactory.java b/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/FormDataTestFactory.java
index 8576eb31d..83b265781 100644
--- a/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/FormDataTestFactory.java
+++ b/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/FormDataTestFactory.java
@@ -5,10 +5,11 @@ import java.util.List;
 import java.util.Map;
 
 import de.ozgcloud.eingang.common.formdata.FormData;
+import de.ozgcloud.eingang.common.formdata.PostfachAddressTestFactory;
 
 public class FormDataTestFactory {
 	public static final String POSTFACH_NAME_ID = "name-id-value";
-	public static final int REST_RESPONSE_NAME_MEMBER_SCOPE_MAILBOX_TYPE_VALUE = 1;
+	public static final int REST_RESPONSE_NAME_MEMBER_SCOPE_MAILBOX_TYPE_VALUE = PostfachAddressTestFactory.POSTFACH_ADDRESS_TYPE;
 
 	public static FormData create() {
 		return createBuilder().build();
-- 
GitLab


From 776814b0ed6b2c0ed639ae251a4d2dabeee2d38a Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Fri, 14 Mar 2025 16:10:15 +0100
Subject: [PATCH 06/28] OZG-7573 add eingang-manager-interface module

---
 eingang-manager-interface/pom.xml             | 174 ++++++++++++++++++
 .../src/main/protobuf/common.model.proto      |  46 +++++
 .../src/main/protobuf/forwarding.model.proto  | 144 +++++++++++++++
 .../src/main/protobuf/forwarding.proto        |  39 ++++
 4 files changed, 403 insertions(+)
 create mode 100644 eingang-manager-interface/pom.xml
 create mode 100644 eingang-manager-interface/src/main/protobuf/common.model.proto
 create mode 100644 eingang-manager-interface/src/main/protobuf/forwarding.model.proto
 create mode 100644 eingang-manager-interface/src/main/protobuf/forwarding.proto

diff --git a/eingang-manager-interface/pom.xml b/eingang-manager-interface/pom.xml
new file mode 100644
index 000000000..dc3858f82
--- /dev/null
+++ b/eingang-manager-interface/pom.xml
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+    Ministerpräsidenten des Landes Schleswig-Holstein
+    Staatskanzlei
+    Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+
+    Lizenziert unter der EUPL, Version 1.2 oder - sobald
+    diese von der Europäischen Kommission genehmigt wurden -
+    Folgeversionen der EUPL ("Lizenz");
+    Sie dürfen dieses Werk ausschließlich gemäß
+    dieser Lizenz nutzen.
+    Eine Kopie der Lizenz finden Sie hier:
+
+    https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+
+    Sofern nicht durch anwendbare Rechtsvorschriften
+    gefordert oder in schriftlicher Form vereinbart, wird
+    die unter der Lizenz verbreitete Software "so wie sie
+    ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+    ausdrücklich oder stillschweigend - verbreitet.
+    Die sprachspezifischen Genehmigungen und Beschränkungen
+    unter der Lizenz sind dem Lizenztext zu entnehmen.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>de.ozgcloud.common</groupId>
+		<artifactId>ozgcloud-common-dependencies</artifactId>
+		<version>4.11.0</version>
+		<relativePath />
+	</parent>
+
+	<groupId>de.ozgcloud.eingang</groupId>
+	<artifactId>eingang-manager-interface</artifactId>
+	<version>2.19.0-SNAPSHOT</version>
+
+	<name>OZG-Cloud Eingang Manager gRPC Interface</name>
+	<description>Interface (gRPC) for Eingang Manager Server</description>
+	<inceptionYear>2025</inceptionYear>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+
+		<java.version>21</java.version>
+		<maven.compiler.source>${java.version}</maven.compiler.source>
+		<maven.compiler.target>${java.version}</maven.compiler.target>
+
+		<find-and-replace-maven-plugin.version>1.1.0</find-and-replace-maven-plugin.version>
+	</properties>
+
+	<dependencyManagement>
+		<dependencies>
+			<dependency>
+				<groupId>de.ozgcloud.common</groupId>
+				<artifactId>ozgcloud-common-dependencies</artifactId>
+				<version>${ozgcloud-common.version}</version>
+				<type>pom</type>
+				<scope>import</scope>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
+	<dependencies>
+		<!-- GRPC -->
+		<dependency>
+			<groupId>io.grpc</groupId>
+			<artifactId>grpc-stub</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>io.grpc</groupId>
+			<artifactId>grpc-protobuf</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>jakarta.annotation</groupId>
+			<artifactId>jakarta.annotation-api</artifactId>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<sourceDirectory>src/main/protobuf</sourceDirectory>
+
+		<extensions>
+			<extension>
+				<groupId>kr.motd.maven</groupId>
+				<artifactId>os-maven-plugin</artifactId>
+			</extension>
+		</extensions>
+
+		<plugins>
+			<plugin>
+				<groupId>com.github.os72</groupId>
+				<artifactId>protoc-jar-maven-plugin</artifactId>
+				<version>${protoc-jar-plugin.version}</version>
+				<executions>
+					<execution>
+						<phase>generate-sources</phase>
+						<goals>
+							<goal>run</goal>
+						</goals>
+						<configuration>
+							<protocVersion>${protobuf.version}</protocVersion>
+							<outputTargets>
+								<outputTarget>
+									<type>java</type>
+								</outputTarget>
+								<outputTarget>
+									<type>grpc-java</type>
+									<pluginArtifact>
+										io.grpc:protoc-gen-grpc-java:${protoc-gen.version}</pluginArtifact>
+								</outputTarget>
+							</outputTargets>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>io.github.floverfelt</groupId>
+				<artifactId>find-and-replace-maven-plugin</artifactId>
+				<version>${find-and-replace-maven-plugin.version}</version>
+				<executions>
+					<execution>
+						<id>exec</id>
+						<phase>process-sources</phase>
+						<goals>
+							<goal>find-and-replace</goal>
+						</goals>
+						<configuration>
+							<replacementType>file-contents</replacementType>
+							<baseDir>target/generated-sources/</baseDir>
+							<findRegex>javax</findRegex>
+							<replaceValue>jakarta</replaceValue>
+							<recursive>true</recursive>
+							<fileMask>.java</fileMask>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-source-plugin</artifactId>
+				<version>3.2.1</version>
+				<executions>
+					<execution>
+						<id>attach-sources</id>
+						<goals>
+							<goal>jar</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+
+	<distributionManagement>
+		<repository>
+			<id>ozg-nexus</id>
+			<name>ozg-releases</name>
+			<url>https://nexus.ozg-sh.de/repository/ozg-releases/</url>
+		</repository>
+		<snapshotRepository>
+			<id>ozg-snapshots-nexus</id>
+			<name>ozg-snapshots</name>
+			<url>https://nexus.ozg-sh.de/repository/ozg-snapshots/</url>
+		</snapshotRepository>
+	</distributionManagement>
+</project>
\ No newline at end of file
diff --git a/eingang-manager-interface/src/main/protobuf/common.model.proto b/eingang-manager-interface/src/main/protobuf/common.model.proto
new file mode 100644
index 000000000..25067f3e9
--- /dev/null
+++ b/eingang-manager-interface/src/main/protobuf/common.model.proto
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+syntax = "proto3";
+
+package de.ozgcloud.eingang.common;
+
+option java_multiple_files = true;
+option java_package = "de.ozgcloud.eingang.common";
+option java_outer_classname = "CommonModelProto";
+
+message GrpcObject {
+  repeated GrpcProperty property = 1;
+  repeated GrpcSubObject subObject = 2;
+}
+
+message GrpcProperty {
+  string name = 1;
+  repeated string value = 2;
+}
+
+message GrpcSubObject {
+  string name = 1;
+  repeated GrpcProperty property = 2;
+  repeated GrpcSubObject subObject = 3;
+}
\ No newline at end of file
diff --git a/eingang-manager-interface/src/main/protobuf/forwarding.model.proto b/eingang-manager-interface/src/main/protobuf/forwarding.model.proto
new file mode 100644
index 000000000..64a281597
--- /dev/null
+++ b/eingang-manager-interface/src/main/protobuf/forwarding.model.proto
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+syntax = "proto3";
+
+package de.ozgcloud.eingang.forwarder;
+
+import "common.model.proto";
+
+option java_multiple_files = true;
+option java_package = "de.ozgcloud.eingang.forwarding";
+option java_outer_classname = "RouteForwardingModelProto";
+
+
+message GrpcRouteForwardingRequest {
+  oneof request {
+    GrpcRouteForwarding routeForwarding = 1;
+    GrpcAttachment attachment = 2;
+    GrpcRepresentation representation = 3;
+  }
+}
+
+message GrpcRouteForwardingResponse {
+
+}
+
+message GrpcRouteForwarding {
+  GrpcEingangStub eingangStub = 1;
+  GrpcRouteCriteria routeCriteria = 2;
+}
+
+message GrpcEingangStub {
+  GrpcEingangHeader header = 1;
+  GrpcAntragsteller antragsteller = 2;
+  GrpcFormData formData = 3;
+}
+
+message GrpcEingangHeader {
+  string requestId = 1;
+  string vorgangNummer = 2;
+  string createdAt = 3;
+  string formId = 4;
+  string formName = 5;
+  string formEngineName = 6;
+  string sender = 7;
+  GrpcServiceKonto serviceKonto = 8;
+}
+
+message GrpcServiceKonto {
+  string type = 1;
+  repeated GrpcPostfachAddress postfachAddresses = 2;
+  string trustLevel = 3;
+}
+
+message GrpcPostfachAddress {
+  string version = 1;
+  de.ozgcloud.eingang.common.GrpcObject identifier = 2;
+  int32 type = 3;
+}
+
+message GrpcAntragsteller {
+  string anrede = 1;
+  string nachname = 2;
+  string vorname = 3;
+  string geburtsdatum = 4;
+  string geburtsort = 5;
+  string geburtsname = 6;
+  string email = 7;
+  string telefon = 8;
+  string strasse = 9;
+  string hausnummer = 10;
+  string plz = 11;
+  string ort = 12;
+  GrpcFormData data = 13;
+  string firmaName = 14;
+}
+
+message GrpcFormData {
+  repeated GrpcFormField field = 1;
+  repeated GrpcFormData form = 2;
+}
+
+message GrpcFormField {
+  string name = 1;
+  string value = 2;
+}
+
+message GrpcRouteCriteria {
+  string organisationseinheitenId = 1;
+}
+
+message GrpcAttachment {
+  oneof attachment {
+    GrpcAttachmentFile file = 1;
+    GrpcFileContent content = 2;
+  }
+}
+
+message GrpcAttachmentFile {
+  string groupName = 1;
+  string fileName = 2;
+  string contentType = 3;
+  string vendorId = 4;
+  int64 size = 5;
+}
+
+message GrpcRepresentation {
+  oneof representation {
+    GrpcRepresentationFile file = 1;
+    GrpcFileContent content = 2;
+  }
+}
+
+message GrpcRepresentationFile {
+  string fileName = 1;
+  string contentType = 2;
+  string vendorId = 3;
+  int64 size = 4;
+}
+
+message GrpcFileContent {
+  bytes content = 1;
+  bool isEndOfFile = 2;
+}
\ No newline at end of file
diff --git a/eingang-manager-interface/src/main/protobuf/forwarding.proto b/eingang-manager-interface/src/main/protobuf/forwarding.proto
new file mode 100644
index 000000000..0d6c4100d
--- /dev/null
+++ b/eingang-manager-interface/src/main/protobuf/forwarding.proto
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+syntax = "proto3";
+
+package de.ozgcloud.eingang.forwarder;
+
+import "forwarding.model.proto";
+
+option java_multiple_files = true;
+option java_package = "de.ozgcloud.eingang.forwarder";
+option java_outer_classname = "RouteForwardingProto";
+
+service RouteForwardingService {
+
+	rpc RouteForwarding(stream GrpcRouteForwardingRequest) returns (GrpcRouteForwardingResponse) {
+	}
+
+}
\ No newline at end of file
-- 
GitLab


From aee9317056988bf957670e61ac25ab7226166efe Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Fri, 14 Mar 2025 16:13:31 +0100
Subject: [PATCH 07/28] OZG-7573 add poc version for
 EingangStubReceiverStreamObserver

---
 forwarder/pom.xml                             |   4 +
 .../EingangStubReceiverStreamObserver.java    | 200 ++++++++++++++++++
 .../forwarder/IncomingFileGroupMapper.java    |  15 ++
 .../eingang/forwarder/IncomingFileMapper.java |  22 ++
 .../eingang/forwarder/RouteCriteria.java      |   2 -
 .../forwarder/RouteForwardingGrpcService.java |   8 +-
 .../forwarder/RouteForwardingMapper.java      |  54 +++++
 ...GrpcRouteForwardingRequestTestFactory.java |   4 +-
 .../forwarder/RouteCriteriaTestFactory.java   |   9 +-
 .../RouteForwardingGrpcServiceTest.java       |  34 +--
 pom.xml                                       |  10 +-
 router/pom.xml                                |   5 +
 12 files changed, 335 insertions(+), 32 deletions(-)
 create mode 100644 forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
 create mode 100644 forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java
 create mode 100644 forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileMapper.java
 create mode 100644 forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java

diff --git a/forwarder/pom.xml b/forwarder/pom.xml
index d79e4343e..4fabecd6a 100644
--- a/forwarder/pom.xml
+++ b/forwarder/pom.xml
@@ -49,6 +49,10 @@
 			<groupId>de.ozgcloud.eingang</groupId>
 			<artifactId>common</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>de.ozgcloud.eingang</groupId>
+			<artifactId>eingang-manager-interface</artifactId>
+		</dependency>
 		<dependency>
 			<groupId>de.ozgcloud.eingang</groupId>
 			<artifactId>router</artifactId>
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
new file mode 100644
index 000000000..13044c3f5
--- /dev/null
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -0,0 +1,200 @@
+package de.ozgcloud.eingang.forwarder;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.apache.commons.io.IOUtils;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.eingang.common.formdata.FormData;
+import de.ozgcloud.eingang.common.formdata.IncomingFile;
+import de.ozgcloud.eingang.forwarding.GrpcAttachment;
+import de.ozgcloud.eingang.forwarding.GrpcFileContent;
+import de.ozgcloud.eingang.forwarding.GrpcRepresentation;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest;
+import io.grpc.stub.StreamObserver;
+import lombok.Builder;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwardingRequest> {
+
+	private static final int CHUNK_SIZE = 1024 * 64;
+	private static final long TIMEOUT_MINUTES = 10;
+	private final RouteForwardingMapper routeForwardingMapper;
+	private final IncomingFileMapper incomingFileMapper;
+	private final IncomingFileGroupMapper incomingFileGroupMapper;
+	private final Function<InputStream, CompletableFuture<File>> fileSaver;
+	private final Consumer<FormData> formDataConsumer;
+
+	@Builder
+	public EingangStubReceiverStreamObserver(RouteForwardingMapper routeForwardingMapper, IncomingFileMapper incomingFileMapper,
+			IncomingFileGroupMapper incomingFileGroupMapper, Function<InputStream, CompletableFuture<File>> fileSaver,
+			Consumer<FormData> formDataConsumer) {
+		this.routeForwardingMapper = routeForwardingMapper;
+		this.incomingFileMapper = incomingFileMapper;
+		this.incomingFileGroupMapper = incomingFileGroupMapper;
+		this.fileSaver = fileSaver;
+		this.formDataConsumer = formDataConsumer;
+	}
+
+	private FormData formData;
+	private List<IncomingFile> representations = new ArrayList<>();
+	private Map<String, List<IncomingFile>> attachments = new HashMap<>();
+
+	private IncomingFile currentFile;
+	private Optional<String> groupName = Optional.empty();
+	private PipedOutputStream pipedOutput;
+	private PipedInputStream pipedInput;
+	private CompletableFuture<File> receivingFileContent;
+
+	@Override
+	public synchronized void onNext(GrpcRouteForwardingRequest request) {
+		if (request.hasRouteForwarding()) {
+			handleRouteForwarding(request.getRouteForwarding());
+		}
+		if (request.hasAttachment()) {
+			handleAttachment(request.getAttachment());
+		}
+		if (request.hasRepresentation()) {
+			handleRepresentation(request.getRepresentation());
+		}
+	}
+
+	private void handleRouteForwarding(GrpcRouteForwarding routeForwarding) {
+		if (Objects.nonNull(formData)) {
+			throw new IllegalStateException("Received second RouteForwarding. Send only one per request.");
+		}
+		formData = routeForwardingMapper.toFormData(routeForwarding.getEingangStub(), routeForwarding.getRouteCriteria());
+	}
+
+	private void handleAttachment(GrpcAttachment attachment) {
+		if (attachment.hasFile()) {
+			setCurrentMetadata(incomingFileMapper.fromGrpcAttachmentFile(attachment.getFile()));
+			groupName = Optional.of(attachment.getFile().getGroupName());
+		} else {
+			if (Objects.isNull(receivingFileContent)) {
+				initContentReceiving();
+			}
+			storeFileContent(attachment.getContent());
+		}
+	}
+
+	private void handleRepresentation(GrpcRepresentation representation) {
+		if (representation.hasFile()) {
+			setCurrentMetadata(incomingFileMapper.fromGrpcRepresentationFile(representation.getFile()));
+		} else {
+			if (Objects.isNull(receivingFileContent)) {
+				initContentReceiving();
+			}
+			storeFileContent(representation.getContent());
+		}
+
+	}
+
+	private void setCurrentMetadata(IncomingFile metaData) {
+		if (Objects.nonNull(currentFile)) {
+			throw new TechnicalException("Received additional file before previos file reached the end.");
+		}
+		currentFile = metaData;
+	}
+
+	private void initContentReceiving() {
+		try {
+			pipedInput = new PipedInputStream(CHUNK_SIZE);
+			pipedOutput = new PipedOutputStream(pipedInput);
+			receivingFileContent = fileSaver.apply(pipedInput);
+		} catch (IOException e) {
+			throw new TechnicalException("Upload initialization failed", e);
+		}
+	}
+
+	private void storeFileContent(GrpcFileContent content) {
+		if (Objects.isNull(currentFile)) {
+			throw new TechnicalException("File content received before metadata.");
+		}
+		try {
+			pipedOutput.write(content.getContent().toByteArray());
+			if (content.getIsEndOfFile()) {
+				handleEndOfFile();
+			}
+		} catch (IOException e) {
+			throw new TechnicalException("Error when writing file content.", e);
+		}
+	}
+
+	private void handleEndOfFile() {
+		closeOutputPipe();
+		var completedIncomingFile = currentFile.toBuilder().file(getSavedFileContent()).build();
+		groupName.map(group -> attachments.get(group)).orElse(representations).add(completedIncomingFile);
+		resetFileReceiving();
+	}
+
+	private File getSavedFileContent() {
+		try {
+			return receivingFileContent.get(TIMEOUT_MINUTES, TimeUnit.MINUTES);
+		} catch (ExecutionException | TimeoutException e) {
+			throw new TechnicalException("Receiving file failed.", e);
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+			throw new TechnicalException("Upload was interrupted.", e);
+		} finally {
+			closeInputPipe();
+		}
+	}
+
+	private void resetFileReceiving() {
+		currentFile = null;
+		groupName = Optional.empty();
+		pipedOutput = null;
+		pipedInput = null;
+		receivingFileContent = null;
+	}
+
+	@Override
+	public void onError(Throwable t) {
+		LOG.error("Error happened. Receiving stream closed.", t);
+		closeOutputPipe();
+		closeInputPipe();
+	}
+
+	private void closeOutputPipe() {
+		IOUtils.closeQuietly(pipedOutput, e -> LOG.error("Cannot close output stream.", e));
+	}
+
+	private void closeInputPipe() {
+		IOUtils.closeQuietly(pipedInput, e -> LOG.error("Cannot close input stream.", e));
+	}
+
+	@Override
+	public void onCompleted() {
+		formDataConsumer.accept(assembleFormData());
+	}
+
+	private FormData assembleFormData() {
+		if (Objects.isNull(formData)) {
+			throw new IllegalStateException("Never received RouteForwarding containing EingangStub and RouteCriteria.");
+		}
+		return formData.toBuilder()
+				.representations(representations)
+				.attachments(attachments.entrySet().stream().map(incomingFileGroupMapper::fromMapEntry).toList())
+				.build();
+	}
+
+}
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java
new file mode 100644
index 000000000..7f35ef41d
--- /dev/null
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java
@@ -0,0 +1,15 @@
+package de.ozgcloud.eingang.forwarder;
+
+import java.util.List;
+import java.util.Map;
+
+import org.mapstruct.Mapper;
+
+import de.ozgcloud.eingang.common.formdata.IncomingFile;
+import de.ozgcloud.eingang.common.formdata.IncomingFileGroup;
+
+@Mapper
+public interface IncomingFileGroupMapper {
+
+	IncomingFileGroup fromMapEntry(Map.Entry<String, List<IncomingFile>> entry); // TODO: Implement this method
+}
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileMapper.java
new file mode 100644
index 000000000..26e3ceddd
--- /dev/null
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileMapper.java
@@ -0,0 +1,22 @@
+package de.ozgcloud.eingang.forwarder;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+import de.ozgcloud.eingang.common.formdata.IncomingFile;
+import de.ozgcloud.eingang.forwarding.GrpcAttachmentFile;
+import de.ozgcloud.eingang.forwarding.GrpcRepresentationFile;
+
+@Mapper
+interface IncomingFileMapper {
+
+	@Mapping(target = "id", ignore = true)
+	@Mapping(target = "file", ignore = true)
+	@Mapping(target = "name", source = "fileName")
+	IncomingFile fromGrpcRepresentationFile(GrpcRepresentationFile representationFile);
+
+	@Mapping(target = "id", ignore = true)
+	@Mapping(target = "file", ignore = true)
+	@Mapping(target = "name", source = "fileName")
+	IncomingFile fromGrpcAttachmentFile(GrpcAttachmentFile representationFile);
+}
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java
index 3a36486c9..65583d7cf 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java
@@ -32,7 +32,5 @@ import lombok.Getter;
 @Builder
 public class RouteCriteria {
 
-	private Optional<String> gemeindeSchluessel;
-	private Optional<String> webserviceUrl;
 	private Optional<String> organisationEinheitId;
 }
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
index 6827f8839..82f8ae332 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
@@ -40,7 +40,11 @@ public class RouteForwardingGrpcService extends RouteForwardingServiceGrpc.Route
 	private final GrpcEingangMapper eingangMapper;
 
 	@Override
-	public void routeForwarding(GrpcRouteForwardingRequest request, StreamObserver<GrpcRouteForwardingResponse> responseObserver) {
-		service.route(criteriaMapper.fromGrpc(request.getRouteCriteria()), eingangMapper.toFormData(request.getEingang()));
+	public StreamObserver<GrpcRouteForwardingRequest> routeForwarding(StreamObserver<GrpcRouteForwardingResponse> responseObserver) {
+		// public void routeForwarding(GrpcRouteForwardingRequest request,
+		// StreamObserver<GrpcRouteForwardingResponse> responseObserver) {
+		// service.route(criteriaMapper.fromGrpc(request.getRouteCriteria()),
+		// eingangMapper.toFormData(request.getEingang()));
+		return null;
 	}
 }
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java
new file mode 100644
index 000000000..315ba8131
--- /dev/null
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.forwarder;
+
+import java.util.Map;
+
+import org.mapstruct.CollectionMappingStrategy;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.NullValueCheckStrategy;
+import org.mapstruct.NullValuePropertyMappingStrategy;
+import org.mapstruct.ReportingPolicy;
+
+import de.ozgcloud.eingang.common.formdata.FormData;
+import de.ozgcloud.eingang.forwarding.GrpcEingangStub;
+import de.ozgcloud.eingang.forwarding.GrpcFormData;
+import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
+
+@Mapper(unmappedTargetPolicy = ReportingPolicy.WARN, //
+		nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, //
+		nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, //
+		collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
+public interface RouteForwardingMapper {
+
+	@Mapping(target = "attachments", ignore = true)
+	@Mapping(target = "representations", ignore = true)
+	@Mapping(target = "control", ignore = true)
+	FormData toFormData(GrpcEingangStub eingangStub, GrpcRouteCriteria routeCriteria); // TODO: Test
+
+	default Map<String, Object> map(GrpcFormData value) {
+		return null; // TODO: Implement
+	};
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
index 37f758e1d..ddd6a44d2 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
@@ -38,8 +38,6 @@ public class GrpcRouteForwardingRequestTestFactory {
 	}
 
 	public static GrpcRouteForwardingRequest.Builder createBuilder() {
-		return GrpcRouteForwardingRequest.newBuilder()
-				.setEingang(EINGANG)
-				.setRouteCriteria(CRITERIA);
+		return GrpcRouteForwardingRequest.newBuilder();
 	}
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java
index 82a9dc490..49c265a75 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java
@@ -24,14 +24,13 @@
 package de.ozgcloud.eingang.forwarder;
 
 import java.util.Optional;
+import java.util.UUID;
 
 import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
 
 public class RouteCriteriaTestFactory {
 
-	public static final String GEMEINDE_SCHLUSSEL = "0815";
-	public static final String WEBSERVICE_URL = "http://nimmerland.by.kop-cloud.de/ws";
-	public static final String ORGANISATION_EINHEIT_ID = "4711";
+	public static final String ORGANISATION_EINHEIT_ID = UUID.randomUUID().toString();
 
 	public static RouteCriteria create() {
 		return createBuilder().build();
@@ -39,8 +38,6 @@ public class RouteCriteriaTestFactory {
 
 	public static RouteCriteria.RouteCriteriaBuilder createBuilder() {
 		return RouteCriteria.builder()
-				.gemeindeSchluessel(Optional.of(GEMEINDE_SCHLUSSEL))
-				.webserviceUrl(Optional.of(WEBSERVICE_URL))
 				.organisationEinheitId(Optional.of(ORGANISATION_EINHEIT_ID));
 	}
 
@@ -50,8 +47,6 @@ public class RouteCriteriaTestFactory {
 
 	public static GrpcRouteCriteria.Builder createGrpcBuilder() {
 		return GrpcRouteCriteria.newBuilder()
-				.setGemeindeSchluessel(GEMEINDE_SCHLUSSEL)
-				.setWebserviceUrl(WEBSERVICE_URL)
 				.setOrganisationseinheitenId(ORGANISATION_EINHEIT_ID);
 	}
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
index e165091ee..f4f0b4f64 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
@@ -28,7 +28,6 @@ import static org.mockito.Mockito.*;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 
@@ -65,26 +64,29 @@ class RouteForwardingGrpcServiceTest {
 			when(eingangMapper.toFormData(any())).thenReturn(formData);
 		}
 
-		@Test
-		void shouldMapCriteria() {
-			service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(), responseObserver);
+		// @Test
+		// void shouldMapCriteria() {
+		// service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(),
+		// responseObserver);
 
-			verify(criteriaMapper).fromGrpc(GrpcRouteForwardingRequestTestFactory.CRITERIA);
-		}
+		// verify(criteriaMapper).fromGrpc(GrpcRouteForwardingRequestTestFactory.CRITERIA);
+		// }
 
-		@Test
-		void shouldMapEingang() {
-			service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(), responseObserver);
+		// @Test
+		// void shouldMapEingang() {
+		// service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(),
+		// responseObserver);
 
-			verify(eingangMapper).toFormData(GrpcRouteForwardingRequestTestFactory.EINGANG);
-		}
+		// verify(eingangMapper).toFormData(GrpcRouteForwardingRequestTestFactory.EINGANG);
+		// }
 
-		@Test
-		void shouldCallService() {
-			service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(), responseObserver);
+		// @Test
+		// void shouldCallService() {
+		// service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(),
+		// responseObserver);
 
-			verify(routeService).route(routeCriteria, formData);
-		}
+		// verify(routeService).route(routeCriteria, formData);
+		// }
 	}
 
 }
diff --git a/pom.xml b/pom.xml
index 4eb2c2578..82d197342 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,7 +33,7 @@
 	<parent>
 		<groupId>de.ozgcloud.common</groupId>
 		<artifactId>ozgcloud-common-parent</artifactId>
-		<version>4.9.0</version>
+		<version>4.11.0</version>
 		<relativePath /> <!-- lookup parent from repository -->
 	</parent>
 
@@ -45,6 +45,7 @@
 
 	<modules>
 		<module>common</module>
+		<module>eingang-manager-interface</module>
 		<module>router</module>
 		<module>forwarder</module>
 		<module>semantik-adapter</module>
@@ -54,7 +55,7 @@
 	</modules>
 
 	<properties>
-		<vorgang-manager.version>2.10.0</vorgang-manager.version>
+		<vorgang-manager.version>2.24.0-OZG-7573-forwarding-interface-SNAPSHOT</vorgang-manager.version>
 		<zufi-manager.version>1.7.0</zufi-manager.version>
 
 		<jsoup.version>1.14.3</jsoup.version>
@@ -75,6 +76,11 @@
 				<artifactId>common</artifactId>
 				<version>${project.version}</version>
 			</dependency>
+			<dependency>
+				<groupId>de.ozgcloud.eingang</groupId>
+				<artifactId>eingang-manager-interface</artifactId>
+				<version>${project.version}</version>
+			</dependency>
 			<dependency>
 				<groupId>de.ozgcloud.eingang</groupId>
 				<artifactId>router</artifactId>
diff --git a/router/pom.xml b/router/pom.xml
index 5964dfb91..95e980d63 100644
--- a/router/pom.xml
+++ b/router/pom.xml
@@ -73,6 +73,11 @@
 			<artifactId>commons-lang3</artifactId>
 		</dependency>
 
+		<dependency>
+			<groupId>org.mapstruct</groupId>
+			<artifactId>mapstruct</artifactId>
+		</dependency>
+
 		<!-- Dev -->
 		<dependency>
 			<groupId>org.projectlombok</groupId>
-- 
GitLab


From 6352b83cdfee7fe1972344e1dfe236e511800016 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 10:32:43 +0100
Subject: [PATCH 08/28] OZG-7573 refactor EingangStubReceiverStreamObserver

---
 .../EingangStubReceiverStreamObserver.java    | 23 +++++++++++--------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index 13044c3f5..202ab2ec4 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -28,6 +28,7 @@ import de.ozgcloud.eingang.forwarding.GrpcFileContent;
 import de.ozgcloud.eingang.forwarding.GrpcRepresentation;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingResponse;
 import io.grpc.stub.StreamObserver;
 import lombok.Builder;
 import lombok.extern.log4j.Log4j2;
@@ -42,16 +43,18 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 	private final IncomingFileGroupMapper incomingFileGroupMapper;
 	private final Function<InputStream, CompletableFuture<File>> fileSaver;
 	private final Consumer<FormData> formDataConsumer;
+	private final Consumer<GrpcRouteForwardingResponse> responseConsumer;
 
 	@Builder
 	public EingangStubReceiverStreamObserver(RouteForwardingMapper routeForwardingMapper, IncomingFileMapper incomingFileMapper,
 			IncomingFileGroupMapper incomingFileGroupMapper, Function<InputStream, CompletableFuture<File>> fileSaver,
-			Consumer<FormData> formDataConsumer) {
+			Consumer<FormData> formDataConsumer, Consumer<GrpcRouteForwardingResponse> responseConsumer) {
 		this.routeForwardingMapper = routeForwardingMapper;
 		this.incomingFileMapper = incomingFileMapper;
 		this.incomingFileGroupMapper = incomingFileGroupMapper;
 		this.fileSaver = fileSaver;
 		this.formDataConsumer = formDataConsumer;
+		this.responseConsumer = responseConsumer;
 	}
 
 	private FormData formData;
@@ -89,10 +92,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 			setCurrentMetadata(incomingFileMapper.fromGrpcAttachmentFile(attachment.getFile()));
 			groupName = Optional.of(attachment.getFile().getGroupName());
 		} else {
-			if (Objects.isNull(receivingFileContent)) {
-				initContentReceiving();
-			}
-			storeFileContent(attachment.getContent());
+			handleFileContent(attachment.getContent());
 		}
 	}
 
@@ -100,10 +100,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		if (representation.hasFile()) {
 			setCurrentMetadata(incomingFileMapper.fromGrpcRepresentationFile(representation.getFile()));
 		} else {
-			if (Objects.isNull(receivingFileContent)) {
-				initContentReceiving();
-			}
-			storeFileContent(representation.getContent());
+			handleFileContent(representation.getContent());
 		}
 
 	}
@@ -115,6 +112,13 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		currentFile = metaData;
 	}
 
+	private void handleFileContent(GrpcFileContent fileContent) {
+		if (Objects.isNull(receivingFileContent)) {
+			initContentReceiving();
+		}
+		storeFileContent(fileContent);
+	}
+
 	private void initContentReceiving() {
 		try {
 			pipedInput = new PipedInputStream(CHUNK_SIZE);
@@ -185,6 +189,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 	@Override
 	public void onCompleted() {
 		formDataConsumer.accept(assembleFormData());
+		responseConsumer.accept(GrpcRouteForwardingResponse.getDefaultInstance());
 	}
 
 	private FormData assembleFormData() {
-- 
GitLab


From f897ea34166a5f67859e2bd11284b16f16ed4106 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 10:33:32 +0100
Subject: [PATCH 09/28] OZG-7573 imlemet RouteForwardingGrpcService with
 EingangStubReceiverStreamObserver

---
 .../eingang/forwarder/FileService.java        |  19 ++
 .../forwarder/RouteForwardingGrpcService.java |  37 +++-
 .../forwarder/RouteForwardingService.java     |   5 +
 .../RouteForwardingGrpcServiceTest.java       | 172 +++++++++++++++---
 4 files changed, 195 insertions(+), 38 deletions(-)
 create mode 100644 forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java
new file mode 100644
index 000000000..812748eb1
--- /dev/null
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java
@@ -0,0 +1,19 @@
+package de.ozgcloud.eingang.forwarder;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+@Service
+class FileService {
+
+	@Async
+	public CompletableFuture<File> saveToFile(InputStream inputStream) {
+		// TODO Auto-generated method stub
+		throw new UnsupportedOperationException("Unimplemented method 'saveToFile'");
+	}
+
+}
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
index 82f8ae332..322e7361f 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
@@ -23,9 +23,12 @@
  */
 package de.ozgcloud.eingang.forwarder;
 
+import java.io.File;
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingResponse;
-import de.ozgcloud.eingang.router.GrpcEingangMapper;
 import io.grpc.stub.StreamObserver;
 import lombok.RequiredArgsConstructor;
 import net.devh.boot.grpc.server.service.GrpcService;
@@ -34,17 +37,31 @@ import net.devh.boot.grpc.server.service.GrpcService;
 @RequiredArgsConstructor
 public class RouteForwardingGrpcService extends RouteForwardingServiceGrpc.RouteForwardingServiceImplBase {
 
-	private final RouteForwardingService service;
-	private final RouteCriteriaMapper criteriaMapper;
-
-	private final GrpcEingangMapper eingangMapper;
+	private final RouteForwardingMapper routeForwardingMapper;
+	private final IncomingFileMapper incomingFileMapper;
+	private final IncomingFileGroupMapper incomingFileGroupMapper;
+	private final RouteForwardingService routeForwardingService;
+	private final FileService fileService;
 
 	@Override
 	public StreamObserver<GrpcRouteForwardingRequest> routeForwarding(StreamObserver<GrpcRouteForwardingResponse> responseObserver) {
-		// public void routeForwarding(GrpcRouteForwardingRequest request,
-		// StreamObserver<GrpcRouteForwardingResponse> responseObserver) {
-		// service.route(criteriaMapper.fromGrpc(request.getRouteCriteria()),
-		// eingangMapper.toFormData(request.getEingang()));
-		return null;
+		return EingangStubReceiverStreamObserver.builder()
+				.fileSaver(this::saveFile)
+				.routeForwardingMapper(routeForwardingMapper)
+				.incomingFileMapper(incomingFileMapper)
+				.incomingFileGroupMapper(incomingFileGroupMapper)
+				.formDataConsumer(routeForwardingService::route)
+				.responseConsumer(repsonse -> respondWith(responseObserver, repsonse))
+				.build();
+	}
+
+	public CompletableFuture<File> saveFile(InputStream inputStream) {
+		return fileService.saveToFile(inputStream);
 	}
+
+	public void respondWith(StreamObserver<GrpcRouteForwardingResponse> responseObserver, GrpcRouteForwardingResponse response) {
+		responseObserver.onNext(response);
+		responseObserver.onCompleted();
+	}
+
 }
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
index 8df1ca5ae..13058871d 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
@@ -45,4 +45,9 @@ class RouteForwardingService {
 						.build())
 				.build());
 	}
+
+	public void route(FormData formData) {
+		// TODO Auto-generated method stub
+		throw new UnsupportedOperationException("Unimplemented method 'route'");
+	}
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
index f4f0b4f64..35ac8140c 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
@@ -23,70 +23,186 @@
  */
 package de.ozgcloud.eingang.forwarder;
 
+import static org.assertj.core.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
-import org.junit.jupiter.api.BeforeEach;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
 import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.thedeanda.lorem.LoremIpsum;
 
 import de.ozgcloud.eingang.common.formdata.FormData;
 import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingResponse;
-import de.ozgcloud.eingang.router.GrpcEingangMapper;
 import io.grpc.stub.StreamObserver;
 
 class RouteForwardingGrpcServiceTest {
 
 	@InjectMocks
+	@Spy
 	private RouteForwardingGrpcService service;
 
 	@Mock
 	private StreamObserver<GrpcRouteForwardingResponse> responseObserver;
 
 	@Mock
-	private RouteForwardingService routeService;
+	private RouteForwardingMapper routeForwardingMapper;
+	@Mock
+	private IncomingFileMapper incomingFileMapper;
 	@Mock
-	private RouteCriteriaMapper criteriaMapper;
+	private IncomingFileGroupMapper incomingFileGroupMapper;
 	@Mock
-	private GrpcEingangMapper eingangMapper;
+	private RouteForwardingService routeForwardingService;
+	@Mock
+	private FileService fileService;
 
 	@Nested
 	class TestRouteForwarding {
+		private static final byte[] CONTENT = LoremIpsum.getInstance().getWords(5).getBytes();
+
+		private static final ByteArrayInputStream INPUT_STREAM = new ByteArrayInputStream(CONTENT);
 
-		private final RouteCriteria routeCriteria = RouteCriteriaTestFactory.create();
 		private final FormData formData = FormDataTestFactory.create();
 
-		@BeforeEach
-		void mock() {
-			when(criteriaMapper.fromGrpc(any())).thenReturn(routeCriteria);
-			when(eingangMapper.toFormData(any())).thenReturn(formData);
+		@Test
+		void shouldSetFileSaver() {
+			var observer = service.routeForwarding(responseObserver);
+
+			callFileSaver(observer);
+			verify(service).saveFile(INPUT_STREAM);
+		}
+
+		@Test
+		void shouldSetRouteForwardingMapper() {
+			var observer = service.routeForwarding(responseObserver);
+
+			assertThat(getRouteForwardingMapper(observer)).isEqualTo(routeForwardingMapper);
+		}
+
+		@Test
+		void shouldSetIncomingFileMapper() {
+			var observer = service.routeForwarding(responseObserver);
+
+			assertThat(getIncomingFileMapper(observer)).isEqualTo(incomingFileMapper);
+		}
+
+		@Test
+		void shouldSetIncomingFileGroupMapper() {
+			var observer = service.routeForwarding(responseObserver);
+
+			assertThat(getIncomingFileGroupMapper(observer)).isEqualTo(incomingFileGroupMapper);
+		}
+
+		@Test
+		void shouldSetFormDataConsumer() {
+			var observer = service.routeForwarding(responseObserver);
+
+			callFormDataConsumer(observer, formData);
+			verify(routeForwardingService).route(formData);
+		}
+
+		@Test
+		void shouldSetResponseConsumer() {
+			var observer = service.routeForwarding(responseObserver);
+
+			callResponseConsumer(observer, GrpcRouteForwardingResponse.getDefaultInstance());
+			verify(service).respondWith(responseObserver, GrpcRouteForwardingResponse.getDefaultInstance());
+		}
+
+		@SuppressWarnings("unchecked")
+		private void callFileSaver(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
+			var fileSaver = (Function<InputStream, CompletableFuture<File>>) ReflectionTestUtils.getField(uploadObserver, "fileSaver");
+			fileSaver.apply(INPUT_STREAM);
+		}
+
+		private RouteForwardingMapper getRouteForwardingMapper(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
+			var routeForwardingMapper = (RouteForwardingMapper) ReflectionTestUtils.getField(uploadObserver, "routeForwardingMapper");
+			return routeForwardingMapper;
+		}
+
+		private IncomingFileMapper getIncomingFileMapper(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
+			var incomingFileMapper = (IncomingFileMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileMapper");
+			return incomingFileMapper;
+		}
+
+		private IncomingFileGroupMapper getIncomingFileGroupMapper(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
+			var incomingFileGroupMapper = (IncomingFileGroupMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileGroupMapper");
+			return incomingFileGroupMapper;
+		}
+
+		@SuppressWarnings("unchecked")
+		private void callFormDataConsumer(StreamObserver<GrpcRouteForwardingRequest> uploadObserver, FormData formData) {
+			var formDataConsumer = (Consumer<FormData>) ReflectionTestUtils.getField(uploadObserver, "formDataConsumer");
+			formDataConsumer.accept(formData);
+		}
+
+		@SuppressWarnings("unchecked")
+		private void callResponseConsumer(StreamObserver<GrpcRouteForwardingRequest> uploadObserver, GrpcRouteForwardingResponse response) {
+			var responseConsumer = (Consumer<GrpcRouteForwardingResponse>) ReflectionTestUtils.getField(uploadObserver, "responseConsumer");
+			responseConsumer.accept(response);
 		}
+	}
+
+	@Nested
+	class TestSaveFile {
 
-		// @Test
-		// void shouldMapCriteria() {
-		// service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(),
-		// responseObserver);
+		@Mock
+		private InputStream inputStream;
+		@Mock
+		private CompletableFuture<File> fileFuture;
 
-		// verify(criteriaMapper).fromGrpc(GrpcRouteForwardingRequestTestFactory.CRITERIA);
-		// }
+		@Test
+		void shouldReturnNull() {
+			service.saveFile(inputStream);
 
-		// @Test
-		// void shouldMapEingang() {
-		// service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(),
-		// responseObserver);
+			verify(fileService).saveToFile(inputStream);
+		}
 
-		// verify(eingangMapper).toFormData(GrpcRouteForwardingRequestTestFactory.EINGANG);
-		// }
+		@Test
+		void shouldReturnFileFuture() {
+			when(fileService.saveToFile(any())).thenReturn(fileFuture);
 
-		// @Test
-		// void shouldCallService() {
-		// service.routeForwarding(GrpcRouteForwardingRequestTestFactory.create(),
-		// responseObserver);
+			var result = service.saveFile(inputStream);
 
-		// verify(routeService).route(routeCriteria, formData);
-		// }
+			assertThat(result).isSameAs(fileFuture);
+		}
 	}
 
+	@Nested
+	class TestRespondWith {
+
+		private GrpcRouteForwardingResponse response = GrpcRouteForwardingResponse.newBuilder().build();
+		@Mock
+		private StreamObserver<GrpcRouteForwardingResponse> responseObserver;
+
+		@Test
+		void shouldCallOnNext() {
+			service.respondWith(responseObserver, response);
+
+			verify(responseObserver).onNext(response);
+		}
+
+		@Test
+		void shouldCallOnCompletedAfterOnNext() {
+			var inOrder = inOrder(responseObserver);
+
+			service.respondWith(responseObserver, response);
+
+			inOrder.verify(responseObserver).onNext(response);
+			inOrder.verify(responseObserver).onCompleted();
+		}
+	}
 }
-- 
GitLab


From b6babd90a0f0fa49473236ee9abc2452957905dc Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 10:46:33 +0100
Subject: [PATCH 10/28] OZG-7573 implement IncomingFileMapper

---
 .../GrpcAttachmentFileTestFactory.java        | 26 ++++++++++++
 .../GrpcRepresentationFileTestFactory.java    | 21 ++++++++++
 .../forwarder/IncomingFileMapperTest.java     | 40 +++++++++++++++++++
 3 files changed, 87 insertions(+)
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationFileTestFactory.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileMapperTest.java

diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java
new file mode 100644
index 000000000..431f4e0db
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java
@@ -0,0 +1,26 @@
+package de.ozgcloud.eingang.forwarder;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
+import de.ozgcloud.eingang.forwarding.GrpcAttachmentFile;
+import de.ozgcloud.eingang.forwarding.GrpcAttachmentFile.Builder;
+
+public class GrpcAttachmentFileTestFactory {
+
+	public static final String GROUP_NAME = LoremIpsum.getInstance().getWords(1);
+
+	public static GrpcAttachmentFile create() {
+		return createBuilder().build();
+	}
+
+	public static Builder createBuilder() {
+		return GrpcAttachmentFile.newBuilder()
+				.setGroupName(GROUP_NAME)
+				.setFileName(IncomingFileTestFactory.NAME)
+				.setContentType(IncomingFileTestFactory.CONTENT_TYPE)
+				.setVendorId(IncomingFileTestFactory.VENDOR_ID)
+				.setSize(IncomingFileTestFactory.SIZE);
+	}
+
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationFileTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationFileTestFactory.java
new file mode 100644
index 000000000..ce7fb5406
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationFileTestFactory.java
@@ -0,0 +1,21 @@
+package de.ozgcloud.eingang.forwarder;
+
+import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
+import de.ozgcloud.eingang.forwarding.GrpcRepresentationFile;
+import de.ozgcloud.eingang.forwarding.GrpcRepresentationFile.Builder;
+
+public class GrpcRepresentationFileTestFactory {
+
+	public static GrpcRepresentationFile create() {
+		return createBuilder().build();
+	}
+
+	public static Builder createBuilder() {
+		return GrpcRepresentationFile.newBuilder()
+				.setFileName(IncomingFileTestFactory.NAME)
+				.setContentType(IncomingFileTestFactory.CONTENT_TYPE)
+				.setVendorId(IncomingFileTestFactory.VENDOR_ID)
+				.setSize(IncomingFileTestFactory.SIZE);
+	}
+
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileMapperTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileMapperTest.java
new file mode 100644
index 000000000..940332b66
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileMapperTest.java
@@ -0,0 +1,40 @@
+package de.ozgcloud.eingang.forwarder;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+
+import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
+
+class IncomingFileMapperTest {
+
+	private final IncomingFileMapper mapper = Mappers.getMapper(IncomingFileMapper.class);
+
+	@Nested
+	class TestFromGrpcRepresentationFile {
+		@Test
+		void shouldMapToIncomingFile() {
+			var grpcRepresentationFile = GrpcRepresentationFileTestFactory.create();
+
+			var incomingFile = mapper.fromGrpcRepresentationFile(grpcRepresentationFile);
+
+			assertThat(incomingFile).usingRecursiveComparison().ignoringFields("id", "file")
+					.isEqualTo(IncomingFileTestFactory.create());
+		}
+	}
+
+	@Nested
+	class TestFromGrpcAttachmentFile {
+		@Test
+		void shouldMapToIncomingFile() {
+			var grpcAttachmentFile = GrpcAttachmentFileTestFactory.create();
+
+			var incomingFile = mapper.fromGrpcAttachmentFile(grpcAttachmentFile);
+
+			assertThat(incomingFile).usingRecursiveComparison().ignoringFields("id", "file")
+					.isEqualTo(IncomingFileTestFactory.create());
+		}
+	}
+}
-- 
GitLab


From ee192e1d0ab2edbc1e51bdac99e4e2dafded3ae4 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 11:10:34 +0100
Subject: [PATCH 11/28] OZG-7573 implement IncomingFileGroupMapper and clean up
 TestFactory

---
 .../IncomingFileGroupTestFactory.java         | 21 ++++----------
 .../formdata/IncomingFileTestFactory.java     |  2 +-
 .../FormCycleFormDataTestFactory.java         |  3 +-
 .../formcycle/FormDataControllerITCase.java   | 13 ++++-----
 .../formcycle/FormDataControllerTest.java     |  2 +-
 .../forwarder/IncomingFileGroupMapper.java    |  7 ++++-
 .../IncomingFileGroupMapperTest.java          | 29 +++++++++++++++++++
 7 files changed, 50 insertions(+), 27 deletions(-)
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapperTest.java

diff --git a/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileGroupTestFactory.java b/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileGroupTestFactory.java
index ebabd58ef..62d245773 100644
--- a/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileGroupTestFactory.java
+++ b/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileGroupTestFactory.java
@@ -25,23 +25,12 @@ package de.ozgcloud.eingang.common.formdata;
 
 import java.util.List;
 
-public class IncomingFileGroupTestFactory {
-	public static final String XDOMEA_XML_NAME = "xdomea.xml";
-	public static final String REPR_XML_NAME = "repr.xml";
-	public static final String REPR_PDF_NAME = "repr.pdf";
-	public static final String ATTATCHMENT_XML_NAME = "att.xml";
-	public static final String ATTATCHMENT_PNG_NAME = "att.png";
-	public static final String ATTATCHMENT_PDF_NAME = "att.pdf";
-
-	public static final String INCOMING_FILE_ID = "xxx";
-	public static final String ID = "id";
-	public static final String FILE_REF1 = "FileRef1";
-	public static final String VENDOR_ID_XXX = "vendorId:xxx";
+import com.thedeanda.lorem.LoremIpsum;
 
-	public static final String NAME = "Ausweis";
+public class IncomingFileGroupTestFactory {
 
-	public static final IncomingFile INCOMING_FILE = IncomingFile.builder().id(INCOMING_FILE_ID).vendorId(VENDOR_ID_XXX).build();
-	public static final List<IncomingFileGroup> FILE_GROUPS = List.of(IncomingFileGroup.builder().files(List.of(INCOMING_FILE)).build());
+	public static final String NAME = LoremIpsum.getInstance().getWords(1);
+	public static final List<IncomingFile> INCOMING_FILES = List.of(IncomingFileTestFactory.create());
 
 	public static IncomingFileGroup create() {
 		return createBuilder().build();
@@ -50,7 +39,7 @@ public class IncomingFileGroupTestFactory {
 	public static IncomingFileGroup.IncomingFileGroupBuilder createBuilder() {
 		return IncomingFileGroup.builder()
 				.name(NAME)
-				.files(List.of(IncomingFileTestFactory.create()));
+				.files(INCOMING_FILES);
 	}
 
 }
diff --git a/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileTestFactory.java b/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileTestFactory.java
index 03fd99127..f377a61cd 100644
--- a/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileTestFactory.java
+++ b/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileTestFactory.java
@@ -35,7 +35,7 @@ import lombok.SneakyThrows;
 public class IncomingFileTestFactory {
 
 	public static final String ID = UUID.randomUUID().toString();
-	public static final String VENDOR_ID = IncomingFileGroupTestFactory.VENDOR_ID_XXX;
+	public static final String VENDOR_ID = "vendorId:xxx";
 	public static final String NAME = "XML-Daten.xml";
 	public static final String CONTENT_TYPE = "application/xml";
 	public static final String PDF_CONTENT_TYPE = MediaType.APPLICATION_PDF_VALUE;
diff --git a/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormCycleFormDataTestFactory.java b/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormCycleFormDataTestFactory.java
index 066b77ac5..edff3cd6c 100644
--- a/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormCycleFormDataTestFactory.java
+++ b/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormCycleFormDataTestFactory.java
@@ -24,6 +24,7 @@
 package de.ozgcloud.eingang.formcycle;
 
 import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory;
+import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
 import de.ozgcloud.eingang.formcycle.FormCycleFormData.Builder;
 import de.ozgcloud.vorgang.common.grpc.GrpcFormDataTestFactory;
 import de.ozgcloud.vorgang.vorgang.GrpcFormData;
@@ -43,7 +44,7 @@ public class FormCycleFormDataTestFactory {
 				.setFormData(GrpcFormDataTestFactory.create())
 				.addAttachmentGroup(FormCycleAttachmentGroup.newBuilder()
 						.setName(IncomingFileGroupTestFactory.NAME)
-						.addFileId(IncomingFileGroupTestFactory.VENDOR_ID_XXX)
+						.addFileId(IncomingFileTestFactory.VENDOR_ID)
 						.build());
 	}
 
diff --git a/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataControllerITCase.java b/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataControllerITCase.java
index f63d7df6e..61daa9f4a 100644
--- a/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataControllerITCase.java
+++ b/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataControllerITCase.java
@@ -37,7 +37,6 @@ import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.ResultActions;
 
 import de.ozgcloud.common.test.ITCase;
-import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory;
 import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
 import de.ozgcloud.eingang.semantik.SemantikAdapter;
 import de.ozgcloud.vorgang.common.grpc.GrpcFormDataTestFactory;
@@ -46,11 +45,11 @@ import de.ozgcloud.vorgang.vorgang.GrpcSubForm;
 import lombok.SneakyThrows;
 
 @ITCase
-//@SpringBootTest(properties = {
-//		"grpc.client.vorgang-manager-local.address=static://127.0.0.1:9090",
-//		"grpc.client.vorgang-manager-local.negotiationType=PLAINTEXT"
-//})
-//@ActiveProfiles("itcase")
+// @SpringBootTest(properties = {
+// "grpc.client.vorgang-manager-local.address=static://127.0.0.1:9090",
+// "grpc.client.vorgang-manager-local.negotiationType=PLAINTEXT"
+// })
+// @ActiveProfiles("itcase")
 @AutoConfigureMockMvc
 class FormDataControllerITCase {
 
@@ -76,7 +75,7 @@ class FormDataControllerITCase {
 							.file(new MockMultipartFile("formData", null, FormDataController.HTTP_TYPE_PROTOBUF, buildTestFormData()))
 							.file(IncomingFileTestFactory.asMultipartFile("representations"))
 							.file(asMultipartFile("attachments",
-									createBuilder().name(IncomingFileGroupTestFactory.VENDOR_ID_XXX + "__" + NAME).build())));
+									createBuilder().name(IncomingFileTestFactory.VENDOR_ID + "__" + NAME).build())));
 		}
 	}
 
diff --git a/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataControllerTest.java b/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataControllerTest.java
index af16a675d..e9fdd387f 100644
--- a/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataControllerTest.java
+++ b/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataControllerTest.java
@@ -239,7 +239,7 @@ class FormDataControllerTest {
 							.file(new MockMultipartFile("formData", null, FormDataController.HTTP_TYPE_PROTOBUF, buildTestFormData()))
 							.file(IncomingFileTestFactory.asMultipartFile("representations"))
 							.file(asMultipartFile("attachments",
-									createBuilder().name(IncomingFileGroupTestFactory.VENDOR_ID_XXX).build())));
+									createBuilder().name(IncomingFileTestFactory.VENDOR_ID).build())));
 		}
 	}
 
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java
index 7f35ef41d..024d2c03e 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java
@@ -11,5 +11,10 @@ import de.ozgcloud.eingang.common.formdata.IncomingFileGroup;
 @Mapper
 public interface IncomingFileGroupMapper {
 
-	IncomingFileGroup fromMapEntry(Map.Entry<String, List<IncomingFile>> entry); // TODO: Implement this method
+	default IncomingFileGroup fromMapEntry(Map.Entry<String, List<IncomingFile>> entry) {
+		return IncomingFileGroup.builder()
+				.name(entry.getKey())
+				.files(entry.getValue())
+				.build();
+	}
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapperTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapperTest.java
new file mode 100644
index 000000000..acdc39b91
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapperTest.java
@@ -0,0 +1,29 @@
+package de.ozgcloud.eingang.forwarder;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+
+import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory;
+
+class IncomingFileGroupMapperTest {
+
+	private final IncomingFileGroupMapper mapper = Mappers.getMapper(IncomingFileGroupMapper.class);
+
+	@Nested
+	class TestFromMapEntry {
+
+		@Test
+		void shouldMapToIncomingFileGroup() {
+			var entry = Map.entry(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.INCOMING_FILES);
+
+			var incomingFileGroup = mapper.fromMapEntry(entry);
+
+			assertThat(incomingFileGroup).usingRecursiveComparison().isEqualTo(IncomingFileGroupTestFactory.create());
+		}
+	}
+}
-- 
GitLab


From d021a5d02b33a128d31f2467d135c8c79501b8ed Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 11:11:16 +0100
Subject: [PATCH 12/28] OZG-7573 set public methods synchronized in
 EingangStubReceiverStreamObserver

---
 .../eingang/forwarder/EingangStubReceiverStreamObserver.java  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index 202ab2ec4..4031741b0 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -172,7 +172,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 	}
 
 	@Override
-	public void onError(Throwable t) {
+	public synchronized void onError(Throwable t) {
 		LOG.error("Error happened. Receiving stream closed.", t);
 		closeOutputPipe();
 		closeInputPipe();
@@ -187,7 +187,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 	}
 
 	@Override
-	public void onCompleted() {
+	public synchronized void onCompleted() {
 		formDataConsumer.accept(assembleFormData());
 		responseConsumer.accept(GrpcRouteForwardingResponse.getDefaultInstance());
 	}
-- 
GitLab


From c37d0fbe778208bc36661b448fb52091c2cc44e2 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 14:10:17 +0100
Subject: [PATCH 13/28] OZG-7573 use vorgang-maanger-interface proto messages

---
 eingang-manager-interface/pom.xml             | 15 +++++
 .../src/main/protobuf/common.model.proto      | 46 ---------------
 ...arding.model.proto => forward.model.proto} | 58 ++-----------------
 .../{forwarding.proto => forward.proto}       |  6 +-
 4 files changed, 22 insertions(+), 103 deletions(-)
 delete mode 100644 eingang-manager-interface/src/main/protobuf/common.model.proto
 rename eingang-manager-interface/src/main/protobuf/{forwarding.model.proto => forward.model.proto} (65%)
 rename eingang-manager-interface/src/main/protobuf/{forwarding.proto => forward.proto} (90%)

diff --git a/eingang-manager-interface/pom.xml b/eingang-manager-interface/pom.xml
index dc3858f82..2aa828f6c 100644
--- a/eingang-manager-interface/pom.xml
+++ b/eingang-manager-interface/pom.xml
@@ -45,6 +45,7 @@
 	<inceptionYear>2025</inceptionYear>
 
 	<properties>
+		<vorgang-manager.version>2.23.0</vorgang-manager.version>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 
@@ -68,6 +69,19 @@
 	</dependencyManagement>
 
 	<dependencies>
+		<!-- OZG-Cloud -->
+		<dependency>
+			<groupId>de.ozgcloud.vorgang</groupId>
+			<artifactId>vorgang-manager-interface</artifactId>
+			<version>${vorgang-manager.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>de.ozgcloud.vorgang</groupId>
+			<artifactId>vorgang-manager-interface</artifactId>
+			<classifier>sources</classifier>
+			<scope>provided</scope>
+			<version>${vorgang-manager.version}</version>
+		</dependency>
 		<!-- GRPC -->
 		<dependency>
 			<groupId>io.grpc</groupId>
@@ -106,6 +120,7 @@
 						</goals>
 						<configuration>
 							<protocVersion>${protobuf.version}</protocVersion>
+							<includeMavenTypes>direct</includeMavenTypes>
 							<outputTargets>
 								<outputTarget>
 									<type>java</type>
diff --git a/eingang-manager-interface/src/main/protobuf/common.model.proto b/eingang-manager-interface/src/main/protobuf/common.model.proto
deleted file mode 100644
index 25067f3e9..000000000
--- a/eingang-manager-interface/src/main/protobuf/common.model.proto
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-syntax = "proto3";
-
-package de.ozgcloud.eingang.common;
-
-option java_multiple_files = true;
-option java_package = "de.ozgcloud.eingang.common";
-option java_outer_classname = "CommonModelProto";
-
-message GrpcObject {
-  repeated GrpcProperty property = 1;
-  repeated GrpcSubObject subObject = 2;
-}
-
-message GrpcProperty {
-  string name = 1;
-  repeated string value = 2;
-}
-
-message GrpcSubObject {
-  string name = 1;
-  repeated GrpcProperty property = 2;
-  repeated GrpcSubObject subObject = 3;
-}
\ No newline at end of file
diff --git a/eingang-manager-interface/src/main/protobuf/forwarding.model.proto b/eingang-manager-interface/src/main/protobuf/forward.model.proto
similarity index 65%
rename from eingang-manager-interface/src/main/protobuf/forwarding.model.proto
rename to eingang-manager-interface/src/main/protobuf/forward.model.proto
index 64a281597..963450aa4 100644
--- a/eingang-manager-interface/src/main/protobuf/forwarding.model.proto
+++ b/eingang-manager-interface/src/main/protobuf/forward.model.proto
@@ -25,7 +25,7 @@ syntax = "proto3";
 
 package de.ozgcloud.eingang.forwarder;
 
-import "common.model.proto";
+import "vorgang.model.proto";
 
 option java_multiple_files = true;
 option java_package = "de.ozgcloud.eingang.forwarding";
@@ -50,59 +50,9 @@ message GrpcRouteForwarding {
 }
 
 message GrpcEingangStub {
-  GrpcEingangHeader header = 1;
-  GrpcAntragsteller antragsteller = 2;
-  GrpcFormData formData = 3;
-}
-
-message GrpcEingangHeader {
-  string requestId = 1;
-  string vorgangNummer = 2;
-  string createdAt = 3;
-  string formId = 4;
-  string formName = 5;
-  string formEngineName = 6;
-  string sender = 7;
-  GrpcServiceKonto serviceKonto = 8;
-}
-
-message GrpcServiceKonto {
-  string type = 1;
-  repeated GrpcPostfachAddress postfachAddresses = 2;
-  string trustLevel = 3;
-}
-
-message GrpcPostfachAddress {
-  string version = 1;
-  de.ozgcloud.eingang.common.GrpcObject identifier = 2;
-  int32 type = 3;
-}
-
-message GrpcAntragsteller {
-  string anrede = 1;
-  string nachname = 2;
-  string vorname = 3;
-  string geburtsdatum = 4;
-  string geburtsort = 5;
-  string geburtsname = 6;
-  string email = 7;
-  string telefon = 8;
-  string strasse = 9;
-  string hausnummer = 10;
-  string plz = 11;
-  string ort = 12;
-  GrpcFormData data = 13;
-  string firmaName = 14;
-}
-
-message GrpcFormData {
-  repeated GrpcFormField field = 1;
-  repeated GrpcFormData form = 2;
-}
-
-message GrpcFormField {
-  string name = 1;
-  string value = 2;
+  de.ozgcloud.vorgang.vorgang.GrpcEingangHeader header = 1;
+  de.ozgcloud.vorgang.vorgang.GrpcAntragsteller antragsteller = 2;
+  de.ozgcloud.vorgang.vorgang.GrpcFormData formData = 3;
 }
 
 message GrpcRouteCriteria {
diff --git a/eingang-manager-interface/src/main/protobuf/forwarding.proto b/eingang-manager-interface/src/main/protobuf/forward.proto
similarity index 90%
rename from eingang-manager-interface/src/main/protobuf/forwarding.proto
rename to eingang-manager-interface/src/main/protobuf/forward.proto
index 0d6c4100d..6d6085eba 100644
--- a/eingang-manager-interface/src/main/protobuf/forwarding.proto
+++ b/eingang-manager-interface/src/main/protobuf/forward.proto
@@ -25,7 +25,7 @@ syntax = "proto3";
 
 package de.ozgcloud.eingang.forwarder;
 
-import "forwarding.model.proto";
+import "forward.model.proto";
 
 option java_multiple_files = true;
 option java_package = "de.ozgcloud.eingang.forwarder";
@@ -33,7 +33,7 @@ option java_outer_classname = "RouteForwardingProto";
 
 service RouteForwardingService {
 
-	rpc RouteForwarding(stream GrpcRouteForwardingRequest) returns (GrpcRouteForwardingResponse) {
-	}
+  rpc RouteForwarding(stream GrpcRouteForwardingRequest) returns (GrpcRouteForwardingResponse) {
+  }
 
 }
\ No newline at end of file
-- 
GitLab


From a4ec99d2d89a13094598aae050c3c8c1abef0ec6 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 14:52:01 +0100
Subject: [PATCH 14/28] OZG-7573 implement RouteForwardingMapper

---
 .../common/formdata/FormDataTestFactory.java  |   6 +-
 .../src/main/protobuf/forward.model.proto     |   2 +-
 .../EingangStubReceiverStreamObserver.java    |   2 +-
 .../eingang/forwarder/RouteCriteria.java      |  36 -------
 .../forwarder/RouteCriteriaMapper.java        |  43 --------
 .../forwarder/RouteForwardingMapper.java      |  36 ++++---
 .../forwarder/RouteForwardingService.java     |  20 ++--
 .../forwarder/GrpcEingangStubTestFactory.java |  28 +++++
 .../GrpcRouteCriteriaTestFactory.java         |  21 ++++
 ...GrpcRouteForwardingRequestTestFactory.java |   2 +-
 .../GrpcRouteForwardingTestFactory.java       |  23 ++++
 .../forwarder/RouteCriteriaTestFactory.java   |  52 ---------
 .../forwarder/RouteForwardingMapperTest.java  | 101 ++++++++++++++++++
 .../forwarder/RouteForwardingServiceTest.java |  56 +++++-----
 .../eingang/router/ServiceKontoMapper.java    |   4 +-
 15 files changed, 241 insertions(+), 191 deletions(-)
 delete mode 100644 forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java
 delete mode 100644 forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteriaMapper.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcEingangStubTestFactory.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteCriteriaTestFactory.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingTestFactory.java
 delete mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapperTest.java

diff --git a/common/src/test/java/de/ozgcloud/eingang/common/formdata/FormDataTestFactory.java b/common/src/test/java/de/ozgcloud/eingang/common/formdata/FormDataTestFactory.java
index 94f756da0..2dc3a7f66 100644
--- a/common/src/test/java/de/ozgcloud/eingang/common/formdata/FormDataTestFactory.java
+++ b/common/src/test/java/de/ozgcloud/eingang/common/formdata/FormDataTestFactory.java
@@ -37,6 +37,8 @@ import lombok.NoArgsConstructor;
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class FormDataTestFactory {
 
+	public static final Antragsteller ANTRAGSTELLER = AntragstellerTestFactory.create();
+	public static final FormHeader HEADER = FormHeaderTestFactory.create();
 	public static final String ID = UUID.randomUUID().toString();
 	public static final String SIMPLE_VALUE_KEY = "kontaktsystemtypid";
 	public static final String SIMPLE_VALUE = "233034600";
@@ -76,8 +78,8 @@ public class FormDataTestFactory {
 	public static FormData.FormDataBuilder createBuilder() {
 		return FormData.builder()
 				.id(ID)
-				.header(FormHeaderTestFactory.create())
-				.antragsteller(AntragstellerTestFactory.create())
+				.header(HEADER)
+				.antragsteller(ANTRAGSTELLER)
 				.zustaendigeStelle(ZustaendigeStelleTestFactory.create())
 				.control(FORM_DATA_CONTROL)
 				.formData(FORM_DATA)
diff --git a/eingang-manager-interface/src/main/protobuf/forward.model.proto b/eingang-manager-interface/src/main/protobuf/forward.model.proto
index 963450aa4..5b0c4a989 100644
--- a/eingang-manager-interface/src/main/protobuf/forward.model.proto
+++ b/eingang-manager-interface/src/main/protobuf/forward.model.proto
@@ -56,7 +56,7 @@ message GrpcEingangStub {
 }
 
 message GrpcRouteCriteria {
-  string organisationseinheitenId = 1;
+  string organisationEinheitId = 1;
 }
 
 message GrpcAttachment {
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index 4031741b0..8f6381157 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -84,7 +84,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		if (Objects.nonNull(formData)) {
 			throw new IllegalStateException("Received second RouteForwarding. Send only one per request.");
 		}
-		formData = routeForwardingMapper.toFormData(routeForwarding.getEingangStub(), routeForwarding.getRouteCriteria());
+		formData = routeForwardingMapper.toFormData(routeForwarding);
 	}
 
 	private void handleAttachment(GrpcAttachment attachment) {
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java
deleted file mode 100644
index 65583d7cf..000000000
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteria.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-package de.ozgcloud.eingang.forwarder;
-
-import java.util.Optional;
-
-import lombok.Builder;
-import lombok.Getter;
-
-@Getter
-@Builder
-public class RouteCriteria {
-
-	private Optional<String> organisationEinheitId;
-}
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteriaMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteriaMapper.java
deleted file mode 100644
index 7fcb6cea6..000000000
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteCriteriaMapper.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-package de.ozgcloud.eingang.forwarder;
-
-import java.util.Optional;
-
-import org.apache.commons.lang3.StringUtils;
-import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
-
-import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
-
-@Mapper
-interface RouteCriteriaMapper {
-
-	@Mapping(target = "organisationEinheitId", source = "organisationseinheitenId")
-	RouteCriteria fromGrpc(GrpcRouteCriteria grpcRouteCriteria);
-
-	default Optional<String> wrapWithOptional(String val) {
-		return Optional.ofNullable(StringUtils.trimToNull(val));
-	}
-}
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java
index 315ba8131..5d519d5c9 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java
@@ -23,32 +23,44 @@
  */
 package de.ozgcloud.eingang.forwarder;
 
-import java.util.Map;
-
 import org.mapstruct.CollectionMappingStrategy;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.NullValueCheckStrategy;
 import org.mapstruct.NullValuePropertyMappingStrategy;
-import org.mapstruct.ReportingPolicy;
 
+import de.ozgcloud.eingang.common.formdata.Antragsteller;
 import de.ozgcloud.eingang.common.formdata.FormData;
-import de.ozgcloud.eingang.forwarding.GrpcEingangStub;
-import de.ozgcloud.eingang.forwarding.GrpcFormData;
+import de.ozgcloud.eingang.common.formdata.ZustaendigeStelle;
 import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
+import de.ozgcloud.eingang.router.ServiceKontoMapper;
+import de.ozgcloud.vorgang.common.grpc.GrpcFormDataMapper;
+import de.ozgcloud.vorgang.vorgang.GrpcAntragsteller;
 
-@Mapper(unmappedTargetPolicy = ReportingPolicy.WARN, //
-		nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, //
+@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, //
 		nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, //
-		collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
+		collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, //
+		uses = { ServiceKontoMapper.class, GrpcFormDataMapper.class })
 public interface RouteForwardingMapper {
 
+	@Mapping(target = "id", ignore = true)
+	@Mapping(target = "attachment", ignore = true)
 	@Mapping(target = "attachments", ignore = true)
+	@Mapping(target = "numberOfAttachments", ignore = true)
+	@Mapping(target = "representation", ignore = true)
 	@Mapping(target = "representations", ignore = true)
+	@Mapping(target = "numberOfRepresentations", ignore = true)
 	@Mapping(target = "control", ignore = true)
-	FormData toFormData(GrpcEingangStub eingangStub, GrpcRouteCriteria routeCriteria); // TODO: Test
+	@Mapping(target = "zustaendigeStelles", ignore = true)
+	@Mapping(target = ".", source = "eingangStub")
+	@Mapping(target = "zustaendigeStelle", source = "routeCriteria")
+	FormData toFormData(GrpcRouteForwarding routeForwarding);
+
+	@Mapping(target = "data", source = "otherData")
+	Antragsteller mapAntragstellerFromGrpc(GrpcAntragsteller antragsteller);
 
-	default Map<String, Object> map(GrpcFormData value) {
-		return null; // TODO: Implement
-	};
+	default ZustaendigeStelle toZustaendigeStelle(GrpcRouteCriteria routeCriteria) {
+		return ZustaendigeStelle.builder().organisationseinheitenId(routeCriteria.getOrganisationEinheitId()).build();
+	}
 }
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
index 13058871d..8a455d31e 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
@@ -26,7 +26,6 @@ package de.ozgcloud.eingang.forwarder;
 import org.springframework.stereotype.Service;
 
 import de.ozgcloud.eingang.common.formdata.FormData;
-import de.ozgcloud.eingang.common.formdata.ZustaendigeStelle;
 import de.ozgcloud.eingang.router.VorgangService;
 import lombok.RequiredArgsConstructor;
 
@@ -36,15 +35,16 @@ class RouteForwardingService {
 
 	private final VorgangService vorgangService;
 
-	public void route(RouteCriteria criteria, FormData formData) {
-		vorgangService.createVorgang(formData.toBuilder()
-				.clearZustaendigeStelles()
-				.zustaendigeStelle(ZustaendigeStelle.builder()
-						.organisationseinheitenId(criteria.getOrganisationEinheitId()
-								.orElseThrow(() -> new UnsupportedOperationException("OrganisationseinheitId is required!")))
-						.build())
-				.build());
-	}
+	// public void route(RouteCriteria criteria, FormData formData) {
+	// vorgangService.createVorgang(formData.toBuilder()
+	// .clearZustaendigeStelles()
+	// .zustaendigeStelle(ZustaendigeStelle.builder()
+	// .organisationseinheitenId(criteria.getOrganisationEinheitId()
+	// .orElseThrow(() -> new UnsupportedOperationException("OrganisationseinheitId
+	// is required!")))
+	// .build())
+	// .build());
+	// }
 
 	public void route(FormData formData) {
 		// TODO Auto-generated method stub
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcEingangStubTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcEingangStubTestFactory.java
new file mode 100644
index 000000000..ac07fe28f
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcEingangStubTestFactory.java
@@ -0,0 +1,28 @@
+package de.ozgcloud.eingang.forwarder;
+
+import de.ozgcloud.eingang.forwarding.GrpcEingangStub;
+import de.ozgcloud.eingang.forwarding.GrpcEingangStub.Builder;
+import de.ozgcloud.eingang.router.GrpcAntragstellerTestFactory;
+import de.ozgcloud.eingang.router.GrpcEingangHeaderTestFactory;
+import de.ozgcloud.vorgang.common.grpc.GrpcFormDataTestFactory;
+import de.ozgcloud.vorgang.vorgang.GrpcAntragsteller;
+import de.ozgcloud.vorgang.vorgang.GrpcEingangHeader;
+import de.ozgcloud.vorgang.vorgang.GrpcFormData;
+
+public class GrpcEingangStubTestFactory {
+
+	public static final GrpcEingangHeader HEADER = GrpcEingangHeaderTestFactory.create();
+	public static final GrpcAntragsteller ANTRAGSTELLER = GrpcAntragstellerTestFactory.create();
+	public static final GrpcFormData FORM_DATA = GrpcFormDataTestFactory.create();
+
+	public static GrpcEingangStub create() {
+		return createBuilder().build();
+	}
+
+	public static Builder createBuilder() {
+		return GrpcEingangStub.newBuilder()
+				.setHeader(HEADER)
+				.setAntragsteller(ANTRAGSTELLER)
+				.setFormData(FORM_DATA);
+	}
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteCriteriaTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteCriteriaTestFactory.java
new file mode 100644
index 000000000..b10b58cc0
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteCriteriaTestFactory.java
@@ -0,0 +1,21 @@
+package de.ozgcloud.eingang.forwarder;
+
+import java.util.UUID;
+
+import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
+import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria.Builder;
+
+public class GrpcRouteCriteriaTestFactory {
+
+	public static final String ORGANISATION_EINHEIT_ID = UUID.randomUUID().toString();
+
+	public static GrpcRouteCriteria create() {
+		return createBuilder().build();
+	}
+
+	public static Builder createBuilder() {
+		return GrpcRouteCriteria.newBuilder()
+				.setOrganisationEinheitId(ORGANISATION_EINHEIT_ID);
+	}
+
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
index ddd6a44d2..9df66980d 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
@@ -31,7 +31,7 @@ import de.ozgcloud.vorgang.vorgang.GrpcEingang;
 public class GrpcRouteForwardingRequestTestFactory {
 
 	public static final GrpcEingang EINGANG = GrpcEingangTestFactory.create();
-	public static final GrpcRouteCriteria CRITERIA = RouteCriteriaTestFactory.createGrpc();
+	public static final GrpcRouteCriteria CRITERIA = GrpcRouteCriteriaTestFactory.create();
 
 	public static GrpcRouteForwardingRequest create() {
 		return createBuilder().build();
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingTestFactory.java
new file mode 100644
index 000000000..76192949b
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingTestFactory.java
@@ -0,0 +1,23 @@
+package de.ozgcloud.eingang.forwarder;
+
+import de.ozgcloud.eingang.forwarding.GrpcEingangStub;
+import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding.Builder;
+
+public class GrpcRouteForwardingTestFactory {
+
+	public static final GrpcRouteCriteria ROUTE_CRITERIA = GrpcRouteCriteriaTestFactory.create();
+	public static final GrpcEingangStub EINGANG_STUB = GrpcEingangStubTestFactory.create();
+
+	public static GrpcRouteForwarding create() {
+		return createBuilder().build();
+	}
+
+	public static Builder createBuilder() {
+		return GrpcRouteForwarding.newBuilder()
+				.setEingangStub(EINGANG_STUB)
+				.setRouteCriteria(ROUTE_CRITERIA);
+	}
+
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java
deleted file mode 100644
index 49c265a75..000000000
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteCriteriaTestFactory.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-package de.ozgcloud.eingang.forwarder;
-
-import java.util.Optional;
-import java.util.UUID;
-
-import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
-
-public class RouteCriteriaTestFactory {
-
-	public static final String ORGANISATION_EINHEIT_ID = UUID.randomUUID().toString();
-
-	public static RouteCriteria create() {
-		return createBuilder().build();
-	}
-
-	public static RouteCriteria.RouteCriteriaBuilder createBuilder() {
-		return RouteCriteria.builder()
-				.organisationEinheitId(Optional.of(ORGANISATION_EINHEIT_ID));
-	}
-
-	public static GrpcRouteCriteria createGrpc() {
-		return createGrpcBuilder().build();
-	}
-
-	public static GrpcRouteCriteria.Builder createGrpcBuilder() {
-		return GrpcRouteCriteria.newBuilder()
-				.setOrganisationseinheitenId(ORGANISATION_EINHEIT_ID);
-	}
-}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapperTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapperTest.java
new file mode 100644
index 000000000..c3475b803
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapperTest.java
@@ -0,0 +1,101 @@
+package de.ozgcloud.eingang.forwarder;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import de.ozgcloud.eingang.common.formdata.AntragstellerTestFactory;
+import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
+import de.ozgcloud.eingang.common.formdata.ServiceKontoTestFactory;
+import de.ozgcloud.eingang.common.formdata.ZustaendigeStelle;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
+import de.ozgcloud.eingang.router.GrpcAntragstellerTestFactory;
+import de.ozgcloud.eingang.router.GrpcEingangHeaderTestFactory;
+import de.ozgcloud.eingang.router.ServiceKontoMapper;
+import de.ozgcloud.vorgang.common.grpc.GrpcFormDataMapper;
+
+class RouteForwardingMapperTest {
+
+	@InjectMocks
+	private RouteForwardingMapper mapper = Mappers.getMapper(RouteForwardingMapper.class);
+
+	@Mock
+	private GrpcFormDataMapper grpcFormDataMapper;
+	@Mock
+	private ServiceKontoMapper serviceKontoMapper;
+
+	@Nested
+	class TestToFormData {
+
+		private final GrpcRouteForwarding routeForwarding = GrpcRouteForwardingTestFactory.create();
+
+		@BeforeEach
+		void mock() {
+			when(serviceKontoMapper.fromGrpc(any())).thenReturn(ServiceKontoTestFactory.create());
+			when(grpcFormDataMapper.mapFromFormData(GrpcAntragstellerTestFactory.OTHER_DATA)).thenReturn(AntragstellerTestFactory.DATA);
+			when(grpcFormDataMapper.mapFromFormData(GrpcEingangStubTestFactory.FORM_DATA)).thenReturn(FormDataTestFactory.FORM_DATA);
+		}
+
+		@Test
+		void shouldCallServiceKontoMapper() {
+			mapper.toFormData(routeForwarding);
+
+			verify(serviceKontoMapper).fromGrpc(GrpcEingangHeaderTestFactory.SERVICE_KONTO);
+		}
+
+		@Test
+		void shouldCallFormDataMapperWithAntragstellerData() {
+			mapper.toFormData(routeForwarding);
+
+			verify(grpcFormDataMapper).mapFromFormData(GrpcAntragstellerTestFactory.OTHER_DATA);
+		}
+
+		@Test
+		void shouldCallFormDataMapperWithFormData() {
+			mapper.toFormData(routeForwarding);
+
+			verify(grpcFormDataMapper).mapFromFormData(GrpcEingangStubTestFactory.FORM_DATA);
+		}
+
+		@Test
+		void shouldMapHeader() {
+			var formDataHeader = mapper.toFormData(routeForwarding).getHeader();
+
+			assertThat(formDataHeader).usingRecursiveComparison().isEqualTo(FormDataTestFactory.HEADER);
+		}
+
+		@Test
+		void shouldMapAntragsteller() {
+			var expectedAntragSteller = AntragstellerTestFactory.createBuilder().firmaName(AntragstellerTestFactory.FIRMA_NAME).build();
+
+			var antragsteller = mapper.toFormData(routeForwarding).getAntragsteller();
+
+			assertThat(antragsteller).usingRecursiveComparison().isEqualTo(expectedAntragSteller);
+		}
+
+		@Test
+		void shouldMapFormData() {
+			var formData = mapper.toFormData(routeForwarding).getFormData();
+
+			assertThat(formData).isSameAs(FormDataTestFactory.FORM_DATA);
+		}
+
+		@Test
+		void shouldMapZustaendigeStelles() {
+			var expectedZustaendigeStelle = ZustaendigeStelle.builder()
+					.organisationseinheitenId(GrpcRouteCriteriaTestFactory.ORGANISATION_EINHEIT_ID)
+					.build();
+
+			var zustaendigeStelles = mapper.toFormData(routeForwarding).getZustaendigeStelles();
+
+			assertThat(zustaendigeStelles).usingRecursiveFieldByFieldElementComparator().containsExactly(expectedZustaendigeStelle);
+		}
+	}
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
index e4c711fbc..f43db02d1 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
@@ -1,13 +1,6 @@
 package de.ozgcloud.eingang.forwarder;
 
-import static org.assertj.core.api.Assertions.*;
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
-
-import java.util.Optional;
-
 import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.InjectMocks;
@@ -15,7 +8,6 @@ import org.mockito.Mock;
 
 import de.ozgcloud.eingang.common.formdata.FormData;
 import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
-import de.ozgcloud.eingang.common.formdata.ZustaendigeStelle;
 import de.ozgcloud.eingang.router.VorgangService;
 
 class RouteForwardingServiceTest {
@@ -32,35 +24,37 @@ class RouteForwardingServiceTest {
 
 		private final FormData formData = FormDataTestFactory.create();
 
-		@Test
-		void shouldCallVorgangService() {
-			var criteria = RouteCriteriaTestFactory.create();
+		// @Test
+		// void shouldCallVorgangService() {
+		// var criteria = RouteCriteriaTestFactory.create();
 
-			routeForwardingService.route(criteria, formData);
+		// routeForwardingService.route(criteria, formData);
 
-			verify(vorgangService).createVorgang(formDataCaptor.capture());
-			assertThat(formDataCaptor.getValue()).usingRecursiveComparison().ignoringFields("zustaendigeStelles").isEqualTo(formData);
-		}
+		// verify(vorgangService).createVorgang(formDataCaptor.capture());
+		// assertThat(formDataCaptor.getValue()).usingRecursiveComparison().ignoringFields("zustaendigeStelles").isEqualTo(formData);
+		// }
 
-		@Test
-		void shouldSetOrganisationEinheitIdInFormData() {
-			var criteria = RouteCriteriaTestFactory.create();
-			var expectedZustaendigeStelle = ZustaendigeStelle.builder()
-					.organisationseinheitenId(RouteCriteriaTestFactory.ORGANISATION_EINHEIT_ID)
-					.build();
+		// @Test
+		// void shouldSetOrganisationEinheitIdInFormData() {
+		// var criteria = RouteCriteriaTestFactory.create();
+		// var expectedZustaendigeStelle = ZustaendigeStelle.builder()
+		// .organisationseinheitenId(RouteCriteriaTestFactory.ORGANISATION_EINHEIT_ID)
+		// .build();
 
-			routeForwardingService.route(criteria, formData);
+		// routeForwardingService.route(criteria, formData);
 
-			verify(vorgangService).createVorgang(formDataCaptor.capture());
-			assertThat(formDataCaptor.getValue().getZustaendigeStelles()).usingRecursiveFieldByFieldElementComparator()
-					.containsExactly(expectedZustaendigeStelle);
-		}
+		// verify(vorgangService).createVorgang(formDataCaptor.capture());
+		// assertThat(formDataCaptor.getValue().getZustaendigeStelles()).usingRecursiveFieldByFieldElementComparator()
+		// .containsExactly(expectedZustaendigeStelle);
+		// }
 
-		@Test
-		void shouldThrowUnsupportedOperationException() {
-			var criteria = RouteCriteriaTestFactory.createBuilder().organisationEinheitId(Optional.empty()).build();
+		// @Test
+		// void shouldThrowUnsupportedOperationException() {
+		// var criteria =
+		// RouteCriteriaTestFactory.createBuilder().organisationEinheitId(Optional.empty()).build();
 
-			assertThrows(UnsupportedOperationException.class, () -> routeForwardingService.route(criteria, formData));
-		}
+		// assertThrows(UnsupportedOperationException.class, () ->
+		// routeForwardingService.route(criteria, formData));
+		// }
 	}
 }
diff --git a/router/src/main/java/de/ozgcloud/eingang/router/ServiceKontoMapper.java b/router/src/main/java/de/ozgcloud/eingang/router/ServiceKontoMapper.java
index 85b8a1685..97aed9ca4 100644
--- a/router/src/main/java/de/ozgcloud/eingang/router/ServiceKontoMapper.java
+++ b/router/src/main/java/de/ozgcloud/eingang/router/ServiceKontoMapper.java
@@ -48,7 +48,7 @@ import de.ozgcloud.vorgang.vorgang.GrpcServiceKonto;
 		nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, //
 		collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, //
 		uses = { GrpcObjectMapper.class })
-abstract class ServiceKontoMapper {
+public abstract class ServiceKontoMapper {
 
 	static final String DEFAULT_TRUST_LEVEL = "STORK-QAA-Level-1";
 
@@ -71,7 +71,7 @@ abstract class ServiceKontoMapper {
 	@Mapping(target = "postfachAddresses", source = "postfachAddressesList")
 	@Mapping(target = "trustLevel", source = "trustLevel", qualifiedByName = "mapTrustLevel")
 	@Mapping(target = "postfachAddress", ignore = true)
-	abstract ServiceKonto fromGrpc(GrpcServiceKonto serviceKonto);
+	public abstract ServiceKonto fromGrpc(GrpcServiceKonto serviceKonto);
 
 	PostfachAddressIdentifier mapToIdentifier(GrpcObject identifier) {
 		var stringIdentifier = MapUtils.getString(grpcObjectMapper.mapFromGrpc(identifier), StringBasedIdentifier.POSTFACH_ID_FIELD);
-- 
GitLab


From 04cd6e35f5fcff5adffc98a75cefbf3ff568b174 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 15:07:05 +0100
Subject: [PATCH 15/28] OZG-7573 license header and clean up

---
 eingang-manager-interface/pom.xml             |  2 +-
 .../src/main/protobuf/forward.model.proto     |  2 +-
 .../src/main/protobuf/forward.proto           |  2 +-
 .../EingangStubReceiverStreamObserver.java    | 23 +++++
 .../eingang/forwarder/FileService.java        | 23 +++++
 .../forwarder/IncomingFileGroupMapper.java    | 23 +++++
 .../eingang/forwarder/IncomingFileMapper.java | 23 +++++
 .../forwarder/RouteForwardingMapper.java      |  2 +-
 .../GrpcAntragstellerTestFactory.java         |  2 +-
 .../GrpcAttachmentFileTestFactory.java        | 23 +++++
 .../forwarder/GrpcEingangStubTestFactory.java | 24 +++++-
 .../GrpcRepresentationFileTestFactory.java    | 23 +++++
 .../GrpcRouteCriteriaTestFactory.java         | 23 +++++
 ...GrpcRouteForwardingRequestTestFactory.java | 43 ----------
 .../GrpcRouteForwardingTestFactory.java       | 23 +++++
 .../IncomingFileGroupMapperTest.java          | 23 +++++
 .../forwarder/IncomingFileMapperTest.java     | 23 +++++
 .../forwarder/RouteForwardingMapperTest.java  | 24 +++++-
 .../forwarder/RouteForwardingServiceTest.java | 23 +++++
 router/pom.xml                                | 11 ---
 .../eingang/router/GrpcEingangMapper.java     | 13 ---
 .../eingang/router/GrpcEingangMapperTest.java | 84 -------------------
 .../router/GrpcEingangTestFactory.java        | 57 -------------
 .../GrpcPostfachAddressTestFactory.java       | 23 +++++
 .../router/GrpcServiceKontoTestFactory.java   | 23 +++++
 .../GrpcZustaendigeStelleTestFactory.java     | 57 -------------
 26 files changed, 350 insertions(+), 272 deletions(-)
 rename {router/src/test/java/de/ozgcloud/eingang/router => forwarder/src/test/java/de/ozgcloud/eingang/forwarder}/GrpcAntragstellerTestFactory.java (98%)
 delete mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
 delete mode 100644 router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangTestFactory.java
 delete mode 100644 router/src/test/java/de/ozgcloud/eingang/router/GrpcZustaendigeStelleTestFactory.java

diff --git a/eingang-manager-interface/pom.xml b/eingang-manager-interface/pom.xml
index 2aa828f6c..b877903e6 100644
--- a/eingang-manager-interface/pom.xml
+++ b/eingang-manager-interface/pom.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-    Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+    Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
     Ministerpräsidenten des Landes Schleswig-Holstein
     Staatskanzlei
     Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
diff --git a/eingang-manager-interface/src/main/protobuf/forward.model.proto b/eingang-manager-interface/src/main/protobuf/forward.model.proto
index 5b0c4a989..29076e289 100644
--- a/eingang-manager-interface/src/main/protobuf/forward.model.proto
+++ b/eingang-manager-interface/src/main/protobuf/forward.model.proto
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
  * Ministerpräsidenten des Landes Schleswig-Holstein
  * Staatskanzlei
  * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
diff --git a/eingang-manager-interface/src/main/protobuf/forward.proto b/eingang-manager-interface/src/main/protobuf/forward.proto
index 6d6085eba..ef695a3e8 100644
--- a/eingang-manager-interface/src/main/protobuf/forward.proto
+++ b/eingang-manager-interface/src/main/protobuf/forward.proto
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
  * Ministerpräsidenten des Landes Schleswig-Holstein
  * Staatskanzlei
  * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index 8f6381157..5c9f3d4ad 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import java.io.File;
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java
index 812748eb1..862aba7b8 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import java.io.File;
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java
index 024d2c03e..ba1939b21 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapper.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import java.util.List;
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileMapper.java
index 26e3ceddd..40176db71 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileMapper.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/IncomingFileMapper.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import org.mapstruct.Mapper;
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java
index 5d519d5c9..3665e7f6c 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
  * Ministerpräsidenten des Landes Schleswig-Holstein
  * Staatskanzlei
  * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcAntragstellerTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAntragstellerTestFactory.java
similarity index 98%
rename from router/src/test/java/de/ozgcloud/eingang/router/GrpcAntragstellerTestFactory.java
rename to forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAntragstellerTestFactory.java
index 61893f6a7..c125f8865 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcAntragstellerTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAntragstellerTestFactory.java
@@ -21,7 +21,7 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-package de.ozgcloud.eingang.router;
+package de.ozgcloud.eingang.forwarder;
 
 import de.ozgcloud.eingang.common.formdata.AntragstellerTestFactory;
 import de.ozgcloud.vorgang.vorgang.GrpcAntragsteller;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java
index 431f4e0db..12382dcfe 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import com.thedeanda.lorem.LoremIpsum;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcEingangStubTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcEingangStubTestFactory.java
index ac07fe28f..f1fc1c664 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcEingangStubTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcEingangStubTestFactory.java
@@ -1,8 +1,30 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import de.ozgcloud.eingang.forwarding.GrpcEingangStub;
 import de.ozgcloud.eingang.forwarding.GrpcEingangStub.Builder;
-import de.ozgcloud.eingang.router.GrpcAntragstellerTestFactory;
 import de.ozgcloud.eingang.router.GrpcEingangHeaderTestFactory;
 import de.ozgcloud.vorgang.common.grpc.GrpcFormDataTestFactory;
 import de.ozgcloud.vorgang.vorgang.GrpcAntragsteller;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationFileTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationFileTestFactory.java
index ce7fb5406..e5155199a 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationFileTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationFileTestFactory.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteCriteriaTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteCriteriaTestFactory.java
index b10b58cc0..450f68612 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteCriteriaTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteCriteriaTestFactory.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import java.util.UUID;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
deleted file mode 100644
index 9df66980d..000000000
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-package de.ozgcloud.eingang.forwarder;
-
-import de.ozgcloud.eingang.forwarding.GrpcRouteCriteria;
-import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest;
-import de.ozgcloud.eingang.router.GrpcEingangTestFactory;
-import de.ozgcloud.vorgang.vorgang.GrpcEingang;
-
-public class GrpcRouteForwardingRequestTestFactory {
-
-	public static final GrpcEingang EINGANG = GrpcEingangTestFactory.create();
-	public static final GrpcRouteCriteria CRITERIA = GrpcRouteCriteriaTestFactory.create();
-
-	public static GrpcRouteForwardingRequest create() {
-		return createBuilder().build();
-	}
-
-	public static GrpcRouteForwardingRequest.Builder createBuilder() {
-		return GrpcRouteForwardingRequest.newBuilder();
-	}
-}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingTestFactory.java
index 76192949b..ca29d219b 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingTestFactory.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import de.ozgcloud.eingang.forwarding.GrpcEingangStub;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapperTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapperTest.java
index acdc39b91..b86144b64 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapperTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileGroupMapperTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import static org.assertj.core.api.Assertions.*;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileMapperTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileMapperTest.java
index 940332b66..c8b12b9f1 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileMapperTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/IncomingFileMapperTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import static org.assertj.core.api.Assertions.*;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapperTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapperTest.java
index c3475b803..406c6e1bd 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapperTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingMapperTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import static org.assertj.core.api.Assertions.*;
@@ -16,7 +39,6 @@ import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
 import de.ozgcloud.eingang.common.formdata.ServiceKontoTestFactory;
 import de.ozgcloud.eingang.common.formdata.ZustaendigeStelle;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
-import de.ozgcloud.eingang.router.GrpcAntragstellerTestFactory;
 import de.ozgcloud.eingang.router.GrpcEingangHeaderTestFactory;
 import de.ozgcloud.eingang.router.ServiceKontoMapper;
 import de.ozgcloud.vorgang.common.grpc.GrpcFormDataMapper;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
index f43db02d1..45015dff9 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import org.junit.jupiter.api.Nested;
diff --git a/router/pom.xml b/router/pom.xml
index 95e980d63..f0659ace4 100644
--- a/router/pom.xml
+++ b/router/pom.xml
@@ -73,11 +73,6 @@
 			<artifactId>commons-lang3</artifactId>
 		</dependency>
 
-		<dependency>
-			<groupId>org.mapstruct</groupId>
-			<artifactId>mapstruct</artifactId>
-		</dependency>
-
 		<!-- Dev -->
 		<dependency>
 			<groupId>org.projectlombok</groupId>
@@ -97,12 +92,6 @@
 			<type>test-jar</type>
 			<scope>test</scope>
 		</dependency>
-		<dependency>
-			<groupId>de.ozgcloud.vorgang</groupId>
-			<artifactId>vorgang-manager-utils</artifactId>
-			<type>test-jar</type>
-			<scope>test</scope>
-		</dependency>
 	</dependencies>
 
 	<build>
diff --git a/router/src/main/java/de/ozgcloud/eingang/router/GrpcEingangMapper.java b/router/src/main/java/de/ozgcloud/eingang/router/GrpcEingangMapper.java
index 382c59752..b5fd75f9a 100644
--- a/router/src/main/java/de/ozgcloud/eingang/router/GrpcEingangMapper.java
+++ b/router/src/main/java/de/ozgcloud/eingang/router/GrpcEingangMapper.java
@@ -82,17 +82,4 @@ public interface GrpcEingangMapper {
 	default String uuidToString(UUID id) {
 		return id.toString();
 	}
-
-	@Mapping(target = "attachment", ignore = true)
-	@Mapping(target = "representation", ignore = true)
-	@Mapping(target = "zustaendigeStelles", ignore = true)
-	@Mapping(target = "control", ignore = true)
-	@Mapping(target = "attachments", source = "attachmentsList")
-	@Mapping(target = "representations", source = "representationsList")
-	@Mapping(target = "antragsteller.data", source = "antragsteller.otherData")
-	FormData toFormData(GrpcEingang eingang);
-
-	@Mapping(target = "files", source = "filesList")
-	@Mapping(target = "file", ignore = true)
-	IncomingFileGroup mapFileGroupFromGrpc(GrpcIncomingFileGroup fileGroup);
 }
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperTest.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperTest.java
index a262e8c7a..eed08a895 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperTest.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangMapperTest.java
@@ -40,7 +40,6 @@ import org.mockito.Mock;
 import de.ozgcloud.eingang.common.formdata.AntragstellerTestFactory;
 import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
 import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
-import de.ozgcloud.eingang.common.formdata.ServiceKontoTestFactory;
 import de.ozgcloud.eingang.common.formdata.ZustaendigeStelleTestFactory;
 import de.ozgcloud.vorgang.common.grpc.GrpcFormDataMapper;
 import de.ozgcloud.vorgang.vorgang.GrpcAntragsteller;
@@ -198,87 +197,4 @@ class GrpcEingangMapperTest {
 			return mapper.toEingang(FormDataTestFactory.create(), Optional.of(ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID));
 		}
 	}
-
-	@Nested
-	class TestToFormData {
-
-		private final GrpcEingang eingang = GrpcEingangTestFactory.create();
-
-		@BeforeEach
-		void mock() {
-			when(serviceKontoMapper.fromGrpc(any())).thenReturn(ServiceKontoTestFactory.create());
-			when(grpcFormDataMapper.mapFromFormData(GrpcAntragstellerTestFactory.OTHER_DATA)).thenReturn(AntragstellerTestFactory.DATA);
-			when(grpcFormDataMapper.mapFromFormData(GrpcEingangTestFactory.FORM_DATA)).thenReturn(FormDataTestFactory.FORM_DATA);
-		}
-
-		@Test
-		void shouldCallServiceKontoMapper() {
-			mapper.toFormData(eingang);
-
-			verify(serviceKontoMapper).fromGrpc(GrpcEingangHeaderTestFactory.SERVICE_KONTO);
-		}
-
-		@Test
-		void shouldCallFormDataMapperWithAntragstellerData() {
-			mapper.toFormData(eingang);
-
-			verify(grpcFormDataMapper).mapFromFormData(GrpcAntragstellerTestFactory.OTHER_DATA);
-		}
-
-		@Test
-		void shouldCallFormDataMapperWithFormData() {
-			mapper.toFormData(eingang);
-
-			verify(grpcFormDataMapper).mapFromFormData(GrpcEingangTestFactory.FORM_DATA);
-		}
-
-		@Test
-		void shouldMapToFormData() {
-			var expectedFormData = FormDataTestFactory.createBuilder()
-					.numberOfAttachments(GrpcEingangTestFactory.NUMBER_OF_ATTACHMENTS)
-					.numberOfRepresentations(GrpcEingangTestFactory.NUMBER_OF_REPRESENTATIONS)
-					.build();
-
-			var formData = mapper.toFormData(eingang);
-
-			assertThat(formData).usingRecursiveComparison()
-					.ignoringFields("antragsteller.firmaName", "control", "attachments", "representations", "zustaendigeStelles")
-					.isEqualTo(expectedFormData);
-		}
-
-		@Nested
-		class TestZustaendigestelles {
-
-			@Test
-			void shouldMapZustaendigeStelles() {
-				var zustaendigeStelles = mapper.toFormData(eingang).getZustaendigeStelles();
-
-				assertThat(zustaendigeStelles).usingRecursiveFieldByFieldElementComparator().containsExactly(ZustaendigeStelleTestFactory.create());
-			}
-		}
-
-		@Nested
-		class TestAttachments {
-
-			@Test
-			void shouldMapAttachments() {
-				var attachments = mapper.toFormData(eingang).getAttachments();
-
-				assertThat(attachments).usingRecursiveFieldByFieldElementComparatorIgnoringFields("files.file")
-						.containsExactly(FormDataTestFactory.ATTACHMENTS.get(0));
-			}
-		}
-
-		@Nested
-		class TestRepresentations {
-
-			@Test
-			void shouldMapRepresentations() {
-				var representations = mapper.toFormData(eingang).getRepresentations();
-
-				assertThat(representations).usingRecursiveFieldByFieldElementComparatorIgnoringFields("file")
-						.containsExactly(FormDataTestFactory.REPRESENTATIONS.get(0));
-			}
-		}
-	}
 }
\ No newline at end of file
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangTestFactory.java
deleted file mode 100644
index 8e15b541a..000000000
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcEingangTestFactory.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-package de.ozgcloud.eingang.router;
-
-import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
-import de.ozgcloud.vorgang.common.grpc.GrpcFormDataTestFactory;
-import de.ozgcloud.vorgang.common.grpc.GrpcSubFormTestFactory;
-import de.ozgcloud.vorgang.vorgang.GrpcEingang;
-import de.ozgcloud.vorgang.vorgang.GrpcFormData;
-import de.ozgcloud.vorgang.vorgang.GrpcSubForm;
-
-public class GrpcEingangTestFactory {
-
-	public static final GrpcFormData FORM_DATA = GrpcFormDataTestFactory.create();
-	public static final int NUMBER_OF_REPRESENTATIONS = 5;
-	public static final int NUMBER_OF_ATTACHMENTS = 4;
-	public static final String ID = FormDataTestFactory.ID;
-	public static final GrpcSubForm SUB_FORM = GrpcSubFormTestFactory.create();
-
-	public static GrpcEingang create() {
-		return createBuilder().build();
-	}
-
-	public static GrpcEingang.Builder createBuilder() {
-		return GrpcEingang.newBuilder()
-				.setId(ID)
-				.setHeader(GrpcEingangHeaderTestFactory.create())
-				.setAntragsteller(GrpcAntragstellerTestFactory.create())
-				.setZustaendigeStelle(GrpcZustaendigeStelleTestFactory.create())
-				.setFormData(FORM_DATA)
-				.addAttachments(GrpcIncomingFileGroupTestFactory.create())
-				.setNumberOfAttachments(NUMBER_OF_ATTACHMENTS)
-				.addRepresentations(GrpcIncomingFileTestFactory.create())
-				.setNumberOfRepresentations(NUMBER_OF_REPRESENTATIONS);
-	}
-}
\ No newline at end of file
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcPostfachAddressTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcPostfachAddressTestFactory.java
index f8ba6cd4b..902a463e9 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcPostfachAddressTestFactory.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcPostfachAddressTestFactory.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.router;
 
 import de.ozgcloud.eingang.common.formdata.PostfachAddressTestFactory;
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcServiceKontoTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcServiceKontoTestFactory.java
index 1995e8123..d223b64c5 100644
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcServiceKontoTestFactory.java
+++ b/router/src/test/java/de/ozgcloud/eingang/router/GrpcServiceKontoTestFactory.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.router;
 
 import de.ozgcloud.eingang.common.formdata.ServiceKontoTestFactory;
diff --git a/router/src/test/java/de/ozgcloud/eingang/router/GrpcZustaendigeStelleTestFactory.java b/router/src/test/java/de/ozgcloud/eingang/router/GrpcZustaendigeStelleTestFactory.java
deleted file mode 100644
index d96403837..000000000
--- a/router/src/test/java/de/ozgcloud/eingang/router/GrpcZustaendigeStelleTestFactory.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-package de.ozgcloud.eingang.router;
-
-import de.ozgcloud.eingang.common.formdata.ZustaendigeStelleTestFactory;
-import de.ozgcloud.vorgang.vorgang.GrpcZustaendigeStelle;
-
-public class GrpcZustaendigeStelleTestFactory {
-
-	public static final String ORGANISATIONSEINHEIT_ID = ZustaendigeStelleTestFactory.ORGANISATIONSEINHEIT_ID;
-	public static final String EMAIL = ZustaendigeStelleTestFactory.EMAIL;
-	public static final String GEMEINDE_SCHLUESSEL = ZustaendigeStelleTestFactory.GEMEINDE_SCHLUESSEL;
-	public static final String AMTLICHER_REGIONAL_SCHLUESSEL = ZustaendigeStelleTestFactory.AMTLICHER_REGIONAL_SCHLUESSEL;
-	public static final String HAUSANSCHRIFT_STRASSE = ZustaendigeStelleTestFactory.HAUSANSCHRIFT_STRASSE;
-	public static final String HAUSANSCHRIFT_PLZ = ZustaendigeStelleTestFactory.HAUSANSCHRIFT_PLZ;
-	public static final String HAUSANSCHRIFT_ORT = ZustaendigeStelleTestFactory.HAUSANSCHRIFT_ORT;
-	public static final String TELEFON = ZustaendigeStelleTestFactory.TELEFON;
-	public static final String BEZEICHNUNG = ZustaendigeStelleTestFactory.BEZEICHNUNG;
-
-	public static GrpcZustaendigeStelle create() {
-		return createBuilder().build();
-	}
-
-	public static GrpcZustaendigeStelle.Builder createBuilder() {
-		return GrpcZustaendigeStelle.newBuilder()
-				.setOrganisationseinheitenId(ORGANISATIONSEINHEIT_ID)
-				.setEmail(EMAIL)
-				.setBezeichnung(BEZEICHNUNG)
-				.setGemeindeSchluessel(GEMEINDE_SCHLUESSEL)
-				.setAmtlicherRegionalSchluessel(AMTLICHER_REGIONAL_SCHLUESSEL)
-				.setHausanschriftStrasse(HAUSANSCHRIFT_STRASSE)
-				.setHausanschriftPlz(HAUSANSCHRIFT_PLZ)
-				.setHausanschriftOrt(HAUSANSCHRIFT_ORT)
-				.setTelefon(TELEFON);
-	}
-}
\ No newline at end of file
-- 
GitLab


From a6ec45afca20e5bcc03b0bb198c05a928f0f2660 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 15:32:57 +0100
Subject: [PATCH 16/28] OZG-7573 implement FileService

---
 .../eingang/forwarder/FileService.java        |  5 ++-
 .../eingang/forwarder/FileServiceTest.java    | 39 +++++++++++++++++++
 2 files changed, 42 insertions(+), 2 deletions(-)
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/FileServiceTest.java

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java
index 862aba7b8..91dcc0d3e 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/FileService.java
@@ -30,13 +30,14 @@ import java.util.concurrent.CompletableFuture;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
+import de.ozgcloud.common.binaryfile.TempFileUtils;
+
 @Service
 class FileService {
 
 	@Async
 	public CompletableFuture<File> saveToFile(InputStream inputStream) {
-		// TODO Auto-generated method stub
-		throw new UnsupportedOperationException("Unimplemented method 'saveToFile'");
+		return CompletableFuture.completedFuture(TempFileUtils.writeTmpFile(inputStream));
 	}
 
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/FileServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/FileServiceTest.java
new file mode 100644
index 000000000..795bcf585
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/FileServiceTest.java
@@ -0,0 +1,39 @@
+package de.ozgcloud.eingang.forwarder;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.nio.file.Files;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Spy;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+import lombok.SneakyThrows;
+
+class FileServiceTest {
+
+	@Spy
+	private FileService service;
+
+	@Nested
+	class TestSaveToFile {
+
+		private final byte[] content = LoremIpsum.getInstance().getWords(100).getBytes();
+
+		@Test
+		@SneakyThrows
+		void shouldReturnFutureOfFile() {
+			try (var inputStream = new ByteArrayInputStream(content);) {
+				var fileFuture = service.saveToFile(inputStream);
+
+				var fileContent = Files.readAllBytes(fileFuture.get().toPath());
+				assertThat(fileContent).isEqualTo(content);
+
+			}
+		}
+
+	}
+}
-- 
GitLab


From 32022c5ca1fb42087449f7bb6f72a3544343cc7b Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Mon, 17 Mar 2025 15:36:29 +0100
Subject: [PATCH 17/28] OZG-7573 implement RouteForwardingService

---
 .../forwarder/RouteForwardingService.java     | 14 +-----
 .../forwarder/RouteForwardingServiceTest.java | 43 ++++---------------
 2 files changed, 9 insertions(+), 48 deletions(-)

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
index 8a455d31e..40e9b7707 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingService.java
@@ -35,19 +35,7 @@ class RouteForwardingService {
 
 	private final VorgangService vorgangService;
 
-	// public void route(RouteCriteria criteria, FormData formData) {
-	// vorgangService.createVorgang(formData.toBuilder()
-	// .clearZustaendigeStelles()
-	// .zustaendigeStelle(ZustaendigeStelle.builder()
-	// .organisationseinheitenId(criteria.getOrganisationEinheitId()
-	// .orElseThrow(() -> new UnsupportedOperationException("OrganisationseinheitId
-	// is required!")))
-	// .build())
-	// .build());
-	// }
-
 	public void route(FormData formData) {
-		// TODO Auto-generated method stub
-		throw new UnsupportedOperationException("Unimplemented method 'route'");
+		vorgangService.createVorgang(formData);
 	}
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
index 45015dff9..4b36715c7 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingServiceTest.java
@@ -23,9 +23,10 @@
  */
 package de.ozgcloud.eingang.forwarder;
 
+import static org.mockito.Mockito.*;
+
 import org.junit.jupiter.api.Nested;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
+import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 
@@ -39,45 +40,17 @@ class RouteForwardingServiceTest {
 	private RouteForwardingService routeForwardingService;
 	@Mock
 	private VorgangService vorgangService;
-	@Captor
-	private ArgumentCaptor<FormData> formDataCaptor;
 
 	@Nested
 	class TestRoute {
 
 		private final FormData formData = FormDataTestFactory.create();
 
-		// @Test
-		// void shouldCallVorgangService() {
-		// var criteria = RouteCriteriaTestFactory.create();
-
-		// routeForwardingService.route(criteria, formData);
-
-		// verify(vorgangService).createVorgang(formDataCaptor.capture());
-		// assertThat(formDataCaptor.getValue()).usingRecursiveComparison().ignoringFields("zustaendigeStelles").isEqualTo(formData);
-		// }
-
-		// @Test
-		// void shouldSetOrganisationEinheitIdInFormData() {
-		// var criteria = RouteCriteriaTestFactory.create();
-		// var expectedZustaendigeStelle = ZustaendigeStelle.builder()
-		// .organisationseinheitenId(RouteCriteriaTestFactory.ORGANISATION_EINHEIT_ID)
-		// .build();
-
-		// routeForwardingService.route(criteria, formData);
-
-		// verify(vorgangService).createVorgang(formDataCaptor.capture());
-		// assertThat(formDataCaptor.getValue().getZustaendigeStelles()).usingRecursiveFieldByFieldElementComparator()
-		// .containsExactly(expectedZustaendigeStelle);
-		// }
-
-		// @Test
-		// void shouldThrowUnsupportedOperationException() {
-		// var criteria =
-		// RouteCriteriaTestFactory.createBuilder().organisationEinheitId(Optional.empty()).build();
+		@Test
+		void shouldCallVorgangService() {
+			routeForwardingService.route(formData);
 
-		// assertThrows(UnsupportedOperationException.class, () ->
-		// routeForwardingService.route(criteria, formData));
-		// }
+			verify(vorgangService).createVorgang(formData);
+		}
 	}
 }
-- 
GitLab


From e4c206f68c9a979fc03375730e21a65d55d4d88b Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Tue, 18 Mar 2025 11:43:46 +0100
Subject: [PATCH 18/28] OZG-7573 implement EingangStubReceiverStreamObserver

---
 .../EingangStubReceiverStreamObserver.java    |   36 +-
 ...EingangStubReceiverStreamObserverTest.java | 1024 +++++++++++++++++
 .../GrpcAttachmentFileTestFactory.java        |    5 +-
 .../forwarder/GrpcAttachmentTestFactory.java  |   47 +
 .../forwarder/GrpcFileContentTestFactory.java |   47 +
 .../GrpcRepresentationTestFactory.java        |   46 +
 ...GrpcRouteForwardingRequestTestFactory.java |   54 +
 7 files changed, 1240 insertions(+), 19 deletions(-)
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentTestFactory.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcFileContentTestFactory.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationTestFactory.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index 5c9f3d4ad..d5f1aa22a 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -103,14 +103,14 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		}
 	}
 
-	private void handleRouteForwarding(GrpcRouteForwarding routeForwarding) {
+	void handleRouteForwarding(GrpcRouteForwarding routeForwarding) {
 		if (Objects.nonNull(formData)) {
 			throw new IllegalStateException("Received second RouteForwarding. Send only one per request.");
 		}
 		formData = routeForwardingMapper.toFormData(routeForwarding);
 	}
 
-	private void handleAttachment(GrpcAttachment attachment) {
+	void handleAttachment(GrpcAttachment attachment) {
 		if (attachment.hasFile()) {
 			setCurrentMetadata(incomingFileMapper.fromGrpcAttachmentFile(attachment.getFile()));
 			groupName = Optional.of(attachment.getFile().getGroupName());
@@ -119,7 +119,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		}
 	}
 
-	private void handleRepresentation(GrpcRepresentation representation) {
+	void handleRepresentation(GrpcRepresentation representation) {
 		if (representation.hasFile()) {
 			setCurrentMetadata(incomingFileMapper.fromGrpcRepresentationFile(representation.getFile()));
 		} else {
@@ -128,21 +128,21 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 
 	}
 
-	private void setCurrentMetadata(IncomingFile metaData) {
+	void setCurrentMetadata(IncomingFile metaData) {
 		if (Objects.nonNull(currentFile)) {
-			throw new TechnicalException("Received additional file before previos file reached the end.");
+			throw new IllegalStateException("Received additional file before previos file reached the end.");
 		}
 		currentFile = metaData;
 	}
 
-	private void handleFileContent(GrpcFileContent fileContent) {
+	void handleFileContent(GrpcFileContent fileContent) {
 		if (Objects.isNull(receivingFileContent)) {
 			initContentReceiving();
 		}
 		storeFileContent(fileContent);
 	}
 
-	private void initContentReceiving() {
+	void initContentReceiving() {
 		try {
 			pipedInput = new PipedInputStream(CHUNK_SIZE);
 			pipedOutput = new PipedOutputStream(pipedInput);
@@ -152,9 +152,9 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		}
 	}
 
-	private void storeFileContent(GrpcFileContent content) {
+	void storeFileContent(GrpcFileContent content) {
 		if (Objects.isNull(currentFile)) {
-			throw new TechnicalException("File content received before metadata.");
+			throw new IllegalStateException("File content received before metadata.");
 		}
 		try {
 			pipedOutput.write(content.getContent().toByteArray());
@@ -166,14 +166,16 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		}
 	}
 
-	private void handleEndOfFile() {
+	void handleEndOfFile() {
 		closeOutputPipe();
 		var completedIncomingFile = currentFile.toBuilder().file(getSavedFileContent()).build();
-		groupName.map(group -> attachments.get(group)).orElse(representations).add(completedIncomingFile);
+		groupName.map(group -> attachments.computeIfAbsent(group, s -> new ArrayList<IncomingFile>()))
+				.orElse(representations)
+				.add(completedIncomingFile);
 		resetFileReceiving();
 	}
 
-	private File getSavedFileContent() {
+	File getSavedFileContent() {
 		try {
 			return receivingFileContent.get(TIMEOUT_MINUTES, TimeUnit.MINUTES);
 		} catch (ExecutionException | TimeoutException e) {
@@ -186,7 +188,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		}
 	}
 
-	private void resetFileReceiving() {
+	void resetFileReceiving() {
 		currentFile = null;
 		groupName = Optional.empty();
 		pipedOutput = null;
@@ -201,11 +203,11 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		closeInputPipe();
 	}
 
-	private void closeOutputPipe() {
+	void closeOutputPipe() {
 		IOUtils.closeQuietly(pipedOutput, e -> LOG.error("Cannot close output stream.", e));
 	}
 
-	private void closeInputPipe() {
+	void closeInputPipe() {
 		IOUtils.closeQuietly(pipedInput, e -> LOG.error("Cannot close input stream.", e));
 	}
 
@@ -215,13 +217,15 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		responseConsumer.accept(GrpcRouteForwardingResponse.getDefaultInstance());
 	}
 
-	private FormData assembleFormData() {
+	FormData assembleFormData() {
 		if (Objects.isNull(formData)) {
 			throw new IllegalStateException("Never received RouteForwarding containing EingangStub and RouteCriteria.");
 		}
 		return formData.toBuilder()
 				.representations(representations)
 				.attachments(attachments.entrySet().stream().map(incomingFileGroupMapper::fromMapEntry).toList())
+				.numberOfAttachments(attachments.size())
+				.numberOfRepresentations(representations.size())
 				.build();
 	}
 
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
new file mode 100644
index 000000000..f4bacd0b2
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
@@ -0,0 +1,1024 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.forwarder;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.eingang.common.formdata.FormData;
+import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
+import de.ozgcloud.eingang.common.formdata.IncomingFile;
+import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory;
+import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
+import de.ozgcloud.eingang.forwarding.GrpcAttachment;
+import de.ozgcloud.eingang.forwarding.GrpcFileContent;
+import de.ozgcloud.eingang.forwarding.GrpcRepresentation;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingResponse;
+import lombok.SneakyThrows;
+
+class EingangStubReceiverStreamObserverTest {
+
+	private EingangStubReceiverStreamObserver observer;
+
+	@Mock
+	private RouteForwardingMapper routeForwardingMapper;
+	@Mock
+	private IncomingFileMapper incomingFileMapper;
+	@Mock
+	private IncomingFileGroupMapper incomingFileGroupMapper;
+	@Mock
+	private Function<InputStream, CompletableFuture<File>> fileSaver;
+	@Mock
+	private Consumer<FormData> formDataConsumer;
+	@Mock
+	private Consumer<GrpcRouteForwardingResponse> responseConsumer;
+
+	@BeforeEach
+	void setUp() {
+		observer = spy(EingangStubReceiverStreamObserver.builder()
+				.fileSaver(fileSaver)
+				.routeForwardingMapper(routeForwardingMapper)
+				.incomingFileMapper(incomingFileMapper)
+				.incomingFileGroupMapper(incomingFileGroupMapper)
+				.formDataConsumer(formDataConsumer)
+				.responseConsumer(responseConsumer)
+				.build());
+	}
+
+	@Nested
+	class TestOnNext {
+
+		@Nested
+		class TestOnRouteForwarding {
+
+			@BeforeEach
+			void mock() {
+				doNothing().when(observer).handleRouteForwarding(any());
+			}
+
+			@Test
+			void shouldCallHandleRouteForwarding() {
+				observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRouteForwarding());
+
+				verify(observer).handleRouteForwarding(GrpcRouteForwardingRequestTestFactory.ROUTE_FORWARDING);
+			}
+
+			@Test
+			void shouldNotCallHandleAttachment() {
+				observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRouteForwarding());
+
+				verify(observer, never()).handleAttachment(any());
+			}
+
+			@Test
+			void shouldNotCallHandleRepresentation() {
+				observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRouteForwarding());
+
+				verify(observer, never()).handleRepresentation(any());
+			}
+		}
+
+		@Nested
+		class TestOnAttachment {
+			@Test
+			void shouldCallHandleAttachment() {
+				observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithAttachment());
+
+				verify(observer).handleAttachment(GrpcRouteForwardingRequestTestFactory.ATTACHMENT);
+			}
+
+			@Test
+			void shouldNotCallHandleRouteForwarding() {
+				observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithAttachment());
+
+				verify(observer, never()).handleRouteForwarding(any());
+			}
+
+			@Test
+			void shouldNotCallHandleRepresentation() {
+				observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithAttachment());
+
+				verify(observer, never()).handleRepresentation(any());
+			}
+		}
+
+		@Nested
+		class TestOnRepresentation {
+			@Test
+			void shouldCallHandleRepresentation() {
+				observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRepresentation());
+
+				verify(observer).handleRepresentation(GrpcRouteForwardingRequestTestFactory.REPRESENTATION);
+			}
+
+			@Test
+			void shouldNotCallHandleRouteForwarding() {
+				observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRepresentation());
+
+				verify(observer, never()).handleRouteForwarding(any());
+			}
+
+			@Test
+			void shouldNotCallHandleAttachment() {
+				observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRepresentation());
+
+				verify(observer, never()).handleAttachment(any());
+			}
+		}
+	}
+
+	@Nested
+	class TestHandleRouteForwarding {
+
+		private final GrpcRouteForwarding routeForwarding = GrpcRouteForwardingTestFactory.create();
+
+		@Test
+		void shouldThrowIllegalStateExceptionIfFormDataIsSet() {
+			setFormData(FormDataTestFactory.create());
+
+			assertThrows(IllegalStateException.class, () -> observer.handleRouteForwarding(routeForwarding));
+
+		}
+
+		@Test
+		void shouldMapRouteForwarding() {
+			observer.handleRouteForwarding(routeForwarding);
+
+			verify(routeForwardingMapper).toFormData(routeForwarding);
+		}
+
+		@Test
+		void shouldSetFormData() {
+			var formData = FormDataTestFactory.create();
+			when(routeForwardingMapper.toFormData(any())).thenReturn(formData);
+
+			observer.handleRouteForwarding(routeForwarding);
+
+			assertThat(getFormData()).isEqualTo(formData);
+		}
+	}
+
+	@Nested
+	class TestHandleAttachment {
+
+		@Nested
+		class TestWithFile {
+
+			private final GrpcAttachment attachmentWithFile = GrpcAttachmentTestFactory.createWithFile();
+			private final IncomingFile incomingFile = IncomingFileTestFactory.create();
+
+			@BeforeEach
+			void mock() {
+				doNothing().when(observer).setCurrentMetadata(any());
+				when(incomingFileMapper.fromGrpcAttachmentFile(any())).thenReturn(incomingFile);
+			}
+
+			@Test
+			void shouldCallIncomingFileMapper() {
+				observer.handleAttachment(attachmentWithFile);
+
+				verify(incomingFileMapper).fromGrpcAttachmentFile(GrpcAttachmentTestFactory.FILE);
+			}
+
+			@Test
+			void shouldCallSetCurrentMetadata() {
+				observer.handleAttachment(attachmentWithFile);
+
+				verify(observer).setCurrentMetadata(incomingFile);
+			}
+
+			@Test
+			void shouldSetGroupName() {
+				observer.handleAttachment(attachmentWithFile);
+
+				assertThat(getGroupName()).contains(GrpcAttachmentFileTestFactory.GROUP_NAME);
+			}
+
+			@Test
+			void shouldNotCallHandleFileContent() {
+				observer.handleAttachment(attachmentWithFile);
+
+				verify(observer, never()).handleFileContent(any());
+			}
+		}
+
+		@Nested
+		class TestWithContent {
+
+			private final GrpcAttachment attachmentWithContent = GrpcAttachmentTestFactory.createWithContent();
+
+			@BeforeEach
+			void mock() {
+				doNothing().when(observer).handleFileContent(any());
+			}
+
+			@Test
+			void shouldCallHandleFileContent() {
+				observer.handleAttachment(attachmentWithContent);
+
+				verify(observer).handleFileContent(GrpcAttachmentTestFactory.CONTENT);
+			}
+
+			@Test
+			void shouldNotCallIncomingFileMapper() {
+				observer.handleAttachment(attachmentWithContent);
+
+				verify(incomingFileMapper, never()).fromGrpcAttachmentFile(any());
+			}
+
+			@Test
+			void shouldNotSetGroupName() {
+				observer.handleAttachment(attachmentWithContent);
+
+				assertThat(getGroupName()).isEmpty();
+			}
+		}
+	}
+
+	@Nested
+	class TestHandleRepresentation {
+
+		@Nested
+		class TestWithFile {
+
+			private final GrpcRepresentation representationWithFile = GrpcRepresentationTestFactory.createWithFile();
+			private final IncomingFile incomingFile = IncomingFileTestFactory.create();
+
+			@BeforeEach
+			void mock() {
+				doNothing().when(observer).setCurrentMetadata(any());
+				when(incomingFileMapper.fromGrpcRepresentationFile(any())).thenReturn(incomingFile);
+			}
+
+			@Test
+			void shouldCallIncomingFileMapper() {
+				observer.handleRepresentation(representationWithFile);
+
+				verify(incomingFileMapper).fromGrpcRepresentationFile(GrpcRepresentationTestFactory.FILE);
+			}
+
+			@Test
+			void shouldCallSetCurrentMetadata() {
+				observer.handleRepresentation(representationWithFile);
+
+				verify(observer).setCurrentMetadata(incomingFile);
+			}
+
+			@Test
+			void shouldNotCallHandleFileContent() {
+				observer.handleRepresentation(representationWithFile);
+
+				verify(observer, never()).handleFileContent(any());
+			}
+		}
+
+		@Nested
+		class TestWithContent {
+
+			private final GrpcRepresentation representationWithContent = GrpcRepresentationTestFactory.createWithContent();
+
+			@BeforeEach
+			void mock() {
+				doNothing().when(observer).handleFileContent(any());
+			}
+
+			@Test
+			void shouldCallHandleFileContent() {
+				observer.handleRepresentation(representationWithContent);
+
+				verify(observer).handleFileContent(GrpcRepresentationTestFactory.CONTENT);
+			}
+
+			@Test
+			void shouldNotCallIncomingFileMapper() {
+				observer.handleRepresentation(representationWithContent);
+
+				verify(incomingFileMapper, never()).fromGrpcRepresentationFile(any());
+			}
+		}
+	}
+
+	@Nested
+	class TestSetCurrentMetadata {
+
+		private final IncomingFile incomingFile = IncomingFileTestFactory.create();
+
+		@Test
+		void shouldThrowIllegalStateExceptionIfCurrentFileIsSet() {
+			setCurrentFile(incomingFile);
+
+			assertThrows(IllegalStateException.class, () -> observer.setCurrentMetadata(incomingFile));
+		}
+
+		@Test
+		void shouldSetCurrentFile() {
+			observer.setCurrentMetadata(incomingFile);
+
+			assertThat(getCurrentFile()).isSameAs(incomingFile);
+		}
+	}
+
+	@Nested
+	class TestHandleFileContent {
+
+		private final GrpcFileContent fileContent = GrpcFileContentTestFactory.create();
+
+		@Nested
+		class TestOnReceivingFileContentIsNull {
+			@BeforeEach
+			void mock() {
+				doNothing().when(observer).initContentReceiving();
+				doNothing().when(observer).storeFileContent(any());
+			}
+
+			@Test
+			void shouldCallInitContentReceiving() {
+				observer.handleFileContent(fileContent);
+
+				verify(observer).initContentReceiving();
+			}
+
+			@Test
+			void shouldCallStoreFileContent() {
+				observer.handleFileContent(fileContent);
+
+				verify(observer).storeFileContent(fileContent);
+			}
+		}
+
+		@Nested
+		class TestOnReceivingFileContentIsNotNull {
+			@Mock
+			private CompletableFuture<File> receivingFileContent;
+
+			@BeforeEach
+			void mock() {
+				doNothing().when(observer).storeFileContent(any());
+				setFileContent(receivingFileContent);
+			}
+
+			@Test
+			void shouldNotCallInitContentReceiving() {
+				observer.handleFileContent(fileContent);
+
+				verify(observer, never()).initContentReceiving();
+			}
+
+			@Test
+			void shouldCallStoreFileContent() {
+				observer.handleFileContent(fileContent);
+
+				verify(observer).storeFileContent(fileContent);
+			}
+		}
+	}
+
+	@Nested
+	class TestInitContentReceiving {
+
+		private final byte[] content = new byte[] { 1, 2, 3 };
+
+		@Test
+		void shouldCreateInputStream() {
+			observer.initContentReceiving();
+
+			assertThat(getPipedInput()).isNotNull();
+		}
+
+		@Test
+		void shouldCreateOutputStream() {
+			observer.initContentReceiving();
+
+			assertThat(getPipedOutput()).isNotNull();
+		}
+
+		@Test
+		void shouldCreateConnectedStreams() {
+			observer.initContentReceiving();
+
+			verifyStreamSetUp();
+		}
+
+		@SneakyThrows
+		private void verifyStreamSetUp() {
+			var pipedInput = getPipedInput();
+			var pipedOutput = getPipedOutput();
+			pipedOutput.write(content);
+			pipedOutput.close();
+			var readBytes = pipedInput.readAllBytes();
+			assertThat(readBytes).isEqualTo(content);
+		}
+
+		@Test
+		void shouldCallFileSaver() {
+			observer.initContentReceiving();
+
+			verify(fileSaver).apply(getPipedInput());
+		}
+
+		@Test
+		void shouldSetReceivingFileContent() {
+			var fileFuture = CompletableFuture.completedFuture(mock(File.class));
+			when(fileSaver.apply(any())).thenReturn(fileFuture);
+
+			observer.initContentReceiving();
+
+			assertThat(getFileContent()).isSameAs(fileFuture);
+		}
+	}
+
+	@Nested
+	class TestStoreFileContent {
+		@Mock
+		private PipedOutputStream pipedOutput;
+
+		@BeforeEach
+		void setUp() {
+			setPipedOutput(pipedOutput);
+		}
+
+		@Nested
+		class TestOnCurrentFileIsNull {
+
+			@Test
+			void shouldThrowTechnicalException() {
+				var fileContent = GrpcFileContentTestFactory.create();
+
+				assertThrows(IllegalStateException.class, () -> observer.storeFileContent(fileContent));
+			}
+		}
+
+		@Nested
+		class TestOnCurrentFileIsNotNull {
+
+			private final IncomingFile incomingFile = IncomingFileTestFactory.create();
+
+			@BeforeEach
+			void mock() {
+				setCurrentFile(incomingFile);
+			}
+
+			@Test
+			@SneakyThrows
+			void shouldWriteContentToOutputStream() {
+				observer.storeFileContent(GrpcFileContentTestFactory.create());
+
+				verify(pipedOutput).write(GrpcFileContentTestFactory.CONTENT);
+			}
+
+			@Test
+			void shouldCallHandleEndOfFile() {
+				doNothing().when(observer).handleEndOfFile();
+				var fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(true).build();
+
+				observer.storeFileContent(fileContent);
+
+				verify(observer).handleEndOfFile();
+			}
+
+			@Test
+			void shouldNotCallHandleEndOfFile() {
+				var fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(false).build();
+
+				observer.storeFileContent(fileContent);
+
+				verify(observer, never()).handleEndOfFile();
+			}
+
+			@Test
+			@SneakyThrows
+			void shouldThrowTechnicalExceptionOnIOException() {
+				doThrow(new IOException()).when(pipedOutput).write(any());
+				var fileContent = GrpcFileContentTestFactory.create();
+
+				assertThrows(TechnicalException.class, () -> {
+					observer.storeFileContent(fileContent);
+				});
+			}
+		}
+	}
+
+	@Nested
+	class TestHandleEndOfFile {
+
+		@Mock
+		private File savedFileContent;
+
+		private final IncomingFile incomingFile = IncomingFileTestFactory.createBuilder().file(null).build();
+
+		@BeforeEach
+		void setUp() {
+			doNothing().when(observer).closeOutputPipe();
+			doReturn(savedFileContent).when(observer).getSavedFileContent();
+			setCurrentFile(incomingFile);
+		}
+
+		@Test
+		void shouldCallCloseOutputPipe() {
+			observer.handleEndOfFile();
+
+			verify(observer).closeOutputPipe();
+		}
+
+		@Nested
+		class TestOnGroupNameEmpty {
+
+			@BeforeEach
+			void setUp() {
+				setGroupName(Optional.empty());
+			}
+
+			@Test
+			void shouldAddFileToRepresentations() {
+				var expectedIncomingFile = IncomingFileTestFactory.createBuilder().file(savedFileContent).build();
+
+				observer.handleEndOfFile();
+
+				assertThat(getRepresentations()).usingRecursiveFieldByFieldElementComparator().containsExactly(expectedIncomingFile);
+			}
+		}
+
+		@Nested
+		class TestOnGroupNameSet {
+
+			@BeforeEach
+			void setUp() {
+				setGroupName(Optional.of(GrpcAttachmentFileTestFactory.GROUP_NAME));
+			}
+
+			@Test
+			void shouldAddFileToAttachments() {
+				var expectedIncomingFile = IncomingFileTestFactory.createBuilder().file(savedFileContent).build();
+
+				observer.handleEndOfFile();
+
+				var attachmentGroup = getAttachments().get(GrpcAttachmentFileTestFactory.GROUP_NAME);
+				assertThat(attachmentGroup).usingRecursiveFieldByFieldElementComparator().containsExactly(expectedIncomingFile);
+			}
+		}
+
+		@Test
+		void shouldCallResetFileReceiving() {
+			observer.handleEndOfFile();
+
+			verify(observer).resetFileReceiving();
+		}
+	}
+
+	@Nested
+	class TestGetSavedFileContent {
+		@BeforeEach
+		void setUp() {
+			doNothing().when(observer).closeInputPipe();
+		}
+
+		@Nested
+		class TestOnNoExceptions {
+			@Mock
+			private File fileContent;
+
+			@BeforeEach
+			void setUp() {
+				setFileContent(CompletableFuture.completedFuture(fileContent));
+			}
+
+			@Test
+			void shouldReturnFile() {
+				var savedFileContent = observer.getSavedFileContent();
+
+				assertThat(savedFileContent).isSameAs(fileContent);
+			}
+
+			@Test
+			void shouldCallCloseInputPipe() {
+				observer.getSavedFileContent();
+
+				verify(observer).closeInputPipe();
+			}
+		}
+
+		@Nested
+		class TestOnExecutionException {
+
+			@BeforeEach
+			void setUp() {
+				setFileContent(CompletableFuture.failedFuture(new Exception()));
+			}
+
+			@Test
+			void shouldThrowTechnicalException() {
+				assertThrows(TechnicalException.class, () -> observer.getSavedFileContent());
+			}
+
+			@Test
+			void shouldCallCloseInputPipe() {
+				try {
+					observer.getSavedFileContent();
+				} catch (TechnicalException e) {
+					// expected
+				}
+
+				verify(observer).closeInputPipe();
+			}
+		}
+
+		@Nested
+		class TestOnTimeoutException {
+
+			@Mock
+			private CompletableFuture<File> fileFuture;
+
+			@BeforeEach
+			@SneakyThrows
+			void setUp() {
+				setFileContent(fileFuture);
+				when(fileFuture.get(anyLong(), any())).thenThrow(new TimeoutException());
+			}
+
+			@Test
+			void shouldThrowTechnicalException() {
+				assertThrows(TechnicalException.class, () -> observer.getSavedFileContent());
+			}
+
+			@Test
+			void shouldCallCloseInputPipe() {
+				try {
+					observer.getSavedFileContent();
+				} catch (TechnicalException e) {
+					// expected
+				}
+
+				verify(observer).closeInputPipe();
+			}
+		}
+
+		@Nested
+		class TestOnInterruptedException {
+
+			@Mock
+			private CompletableFuture<File> fileFuture;
+
+			@BeforeEach
+			@SneakyThrows
+			void setUp() {
+				setFileContent(fileFuture);
+				when(fileFuture.get(anyLong(), any())).thenThrow(new InterruptedException());
+			}
+
+			@Test
+			void shouldThrowTechnicalException() {
+				assertThrows(TechnicalException.class, () -> observer.getSavedFileContent());
+			}
+
+			@Test
+			void shouldInterruptCurrentThread() {
+				try {
+					observer.getSavedFileContent();
+				} catch (TechnicalException e) {
+					// expected
+				}
+
+				assertThat(Thread.currentThread().isInterrupted()).isTrue();
+			}
+
+			@Test
+			void shouldCallCloseInputPipe() {
+				try {
+					observer.getSavedFileContent();
+				} catch (TechnicalException e) {
+					// expected
+				}
+
+				verify(observer).closeInputPipe();
+			}
+		}
+	}
+
+	@Nested
+	class TestResetFielReceiving {
+
+		@BeforeEach
+		void setUp() {
+			setCurrentFile(IncomingFileTestFactory.create());
+			setGroupName(Optional.of(GrpcAttachmentFileTestFactory.GROUP_NAME));
+			setPipedOutput(mock(PipedOutputStream.class));
+			setPipedOutput(mock(PipedOutputStream.class));
+			setFileContent(CompletableFuture.completedFuture(mock(File.class)));
+		}
+
+		@Test
+		void shouldResetCurrentFile() {
+			observer.resetFileReceiving();
+
+			assertThat(getCurrentFile()).isNull();
+		}
+
+		@Test
+		void shouldResetGroupName() {
+			observer.resetFileReceiving();
+
+			assertThat(getGroupName()).isEmpty();
+		}
+
+		@Test
+		void shouldResetPipedOutput() {
+			observer.resetFileReceiving();
+
+			assertThat(getPipedOutput()).isNull();
+		}
+
+		@Test
+		void shouldResetPipedInput() {
+			observer.resetFileReceiving();
+
+			assertThat(getPipedInput()).isNull();
+		}
+
+		@Test
+		void shouldResetReceivingFileContent() {
+			observer.resetFileReceiving();
+
+			assertThat(getFileContent()).isNull();
+		}
+	}
+
+	@Nested
+	class TestOnError {
+
+		@BeforeEach
+		void mock() {
+			doNothing().when(observer).closeOutputPipe();
+			doNothing().when(observer).closeInputPipe();
+		}
+
+		@Test
+		void shouldCallCloseOutputPipe() {
+			observer.onError(new Exception());
+
+			verify(observer).closeOutputPipe();
+		}
+
+		@Test
+		void shouldCallCloseInputPipe() {
+			observer.onError(new Exception());
+
+			verify(observer).closeInputPipe();
+		}
+	}
+
+	@Nested
+	class TestCloseOutputPipe {
+
+		@Mock
+		private PipedOutputStream pipedOutput;
+
+		@BeforeEach
+		void setUp() {
+			setPipedOutput(pipedOutput);
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldClosePipedOutput() {
+			observer.closeOutputPipe();
+
+			verify(pipedOutput).close();
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldNotThrowException() {
+			doThrow(IOException.class).when(pipedOutput).close();
+
+			assertDoesNotThrow(() -> observer.closeOutputPipe());
+		}
+	}
+
+	@Nested
+	class TestCloseInputPipe {
+
+		@Mock
+		private PipedInputStream pipedInput;
+
+		@BeforeEach
+		void setUp() {
+			setPipedInput(pipedInput);
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldClosePipedInput() {
+			observer.closeInputPipe();
+
+			verify(pipedInput).close();
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldNotThrowException() {
+			doThrow(IOException.class).when(pipedInput).close();
+
+			assertDoesNotThrow(() -> observer.closeInputPipe());
+		}
+	}
+
+	@Nested
+	class TestOnCompleted {
+
+		private final FormData formData = FormDataTestFactory.create();
+
+		@BeforeEach
+		void mock() {
+			doReturn(formData).when(observer).assembleFormData();
+		}
+
+		@Test
+		void shouldCallAssembleFormData() {
+			observer.onCompleted();
+
+			verify(observer).assembleFormData();
+		}
+
+		@Test
+		void shouldCallFormDataConsumer() {
+			observer.onCompleted();
+
+			verify(formDataConsumer).accept(formData);
+		}
+
+		@Test
+		void shouldCallResponseConsumer() {
+			observer.onCompleted();
+
+			verify(responseConsumer).accept(GrpcRouteForwardingResponse.getDefaultInstance());
+		}
+	}
+
+	@Nested
+	class TestAssembleFormData {
+
+		@Nested
+		class TestOnFormDataNotSet {
+
+			@Test
+			void shouldThrowIllegalStateException() {
+				assertThrows(IllegalStateException.class, () -> observer.assembleFormData());
+			}
+		}
+
+		@Nested
+		class TestOnFormDataSet {
+
+			private final FormData formData = FormDataTestFactory.createBuilder()
+					.clearAttachments()
+					.clearRepresentations()
+					.numberOfAttachments(0)
+					.numberOfRepresentations(0)
+					.control(null)
+					.build();
+
+			private final List<IncomingFile> representations = FormDataTestFactory.REPRESENTATIONS;
+			private final Map.Entry<String, List<IncomingFile>> attachmentEntry = Map.entry(
+					IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.INCOMING_FILES);
+
+			@BeforeEach
+			void setUp() {
+				setFormData(formData);
+				setRepresentations(representations);
+				setAttachments(Map.ofEntries(attachmentEntry));
+			}
+
+			@Test
+			void shouldCallIncomingFileGroupMapper() {
+				observer.assembleFormData();
+
+				verify(incomingFileGroupMapper).fromMapEntry(attachmentEntry);
+			}
+
+			@Test
+			void shouldReturnFormData() {
+				var expectedFormData = FormDataTestFactory.createBuilder()
+						.control(null)
+						.build();
+				when(incomingFileGroupMapper.fromMapEntry(attachmentEntry)).thenReturn(IncomingFileGroupTestFactory.create());
+
+				var assembledFormData = observer.assembleFormData();
+
+				assertThat(assembledFormData).usingRecursiveComparison().isEqualTo(expectedFormData);
+			}
+		}
+	}
+
+	private FormData getFormData() {
+		return (FormData) ReflectionTestUtils.getField(observer, "formData");
+	}
+
+	private void setFormData(FormData formData) {
+		ReflectionTestUtils.setField(observer, "formData", formData);
+	}
+
+	@SuppressWarnings("unchecked")
+	private Optional<String> getGroupName() {
+		return (Optional<String>) ReflectionTestUtils.getField(observer, "groupName");
+	}
+
+	private void setGroupName(Optional<String> groupName) {
+		ReflectionTestUtils.setField(observer, "groupName", groupName);
+	}
+
+	private IncomingFile getCurrentFile() {
+		return (IncomingFile) ReflectionTestUtils.getField(observer, "currentFile");
+	}
+
+	private void setCurrentFile(IncomingFile incomingFile) {
+		ReflectionTestUtils.setField(observer, "currentFile", incomingFile);
+	}
+
+	private void setFileContent(CompletableFuture<File> fileFuture) {
+		ReflectionTestUtils.setField(observer, "receivingFileContent", fileFuture);
+	}
+
+	@SuppressWarnings("unchecked")
+	private CompletableFuture<File> getFileContent() {
+		return (CompletableFuture<File>) ReflectionTestUtils.getField(observer, "receivingFileContent");
+	}
+
+	private PipedInputStream getPipedInput() {
+		return (PipedInputStream) ReflectionTestUtils.getField(observer, "pipedInput");
+	}
+
+	private void setPipedInput(PipedInputStream pipedInput) {
+		ReflectionTestUtils.setField(observer, "pipedInput", pipedInput);
+	}
+
+	private PipedOutputStream getPipedOutput() {
+		return (PipedOutputStream) ReflectionTestUtils.getField(observer, "pipedOutput");
+	}
+
+	private void setPipedOutput(PipedOutputStream pipedOutput) {
+		ReflectionTestUtils.setField(observer, "pipedOutput", pipedOutput);
+	}
+
+	@SuppressWarnings("unchecked")
+	private List<IncomingFile> getRepresentations() {
+		return (List<IncomingFile>) ReflectionTestUtils.getField(observer, "representations");
+	}
+
+	private void setRepresentations(List<IncomingFile> representations) {
+		ReflectionTestUtils.setField(observer, "representations", representations);
+	}
+
+	@SuppressWarnings("unchecked")
+	private Map<String, List<IncomingFile>> getAttachments() {
+		return (Map<String, List<IncomingFile>>) ReflectionTestUtils.getField(observer, "attachments");
+	}
+
+	private void setAttachments(Map<String, List<IncomingFile>> attachments) {
+		ReflectionTestUtils.setField(observer, "attachments", attachments);
+	}
+
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java
index 12382dcfe..ec48f44e4 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java
@@ -23,15 +23,14 @@
  */
 package de.ozgcloud.eingang.forwarder;
 
-import com.thedeanda.lorem.LoremIpsum;
-
+import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory;
 import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
 import de.ozgcloud.eingang.forwarding.GrpcAttachmentFile;
 import de.ozgcloud.eingang.forwarding.GrpcAttachmentFile.Builder;
 
 public class GrpcAttachmentFileTestFactory {
 
-	public static final String GROUP_NAME = LoremIpsum.getInstance().getWords(1);
+	public static final String GROUP_NAME = IncomingFileGroupTestFactory.NAME;
 
 	public static GrpcAttachmentFile create() {
 		return createBuilder().build();
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentTestFactory.java
new file mode 100644
index 000000000..e5593e13f
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentTestFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.forwarder;
+
+import de.ozgcloud.eingang.forwarding.GrpcAttachment;
+import de.ozgcloud.eingang.forwarding.GrpcAttachmentFile;
+import de.ozgcloud.eingang.forwarding.GrpcFileContent;
+
+public class GrpcAttachmentTestFactory {
+
+	public static final GrpcAttachmentFile FILE = GrpcAttachmentFileTestFactory.create();
+	public static final GrpcFileContent CONTENT = GrpcFileContentTestFactory.create();
+
+	public static GrpcAttachment createWithFile() {
+		return GrpcAttachment.newBuilder()
+				.setFile(FILE)
+				.build();
+	}
+
+	public static GrpcAttachment createWithContent() {
+		return GrpcAttachment.newBuilder()
+				.setContent(CONTENT)
+				.build();
+	}
+
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcFileContentTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcFileContentTestFactory.java
new file mode 100644
index 000000000..d4040f975
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcFileContentTestFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.forwarder;
+
+import com.google.protobuf.ByteString;
+import com.thedeanda.lorem.LoremIpsum;
+
+import de.ozgcloud.eingang.forwarding.GrpcFileContent;
+import de.ozgcloud.eingang.forwarding.GrpcFileContent.Builder;
+
+public class GrpcFileContentTestFactory {
+
+	public static final boolean IS_END_OF_FILE = false;
+	public static final byte[] CONTENT = LoremIpsum.getInstance().getWords(10).getBytes();
+
+	public static GrpcFileContent create() {
+		return createBuilder().build();
+	}
+
+	public static Builder createBuilder() {
+		return GrpcFileContent.newBuilder()
+				.setContent(ByteString.copyFrom(CONTENT))
+				.setIsEndOfFile(IS_END_OF_FILE);
+	}
+
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationTestFactory.java
new file mode 100644
index 000000000..e437c900d
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationTestFactory.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.forwarder;
+
+import de.ozgcloud.eingang.forwarding.GrpcFileContent;
+import de.ozgcloud.eingang.forwarding.GrpcRepresentation;
+import de.ozgcloud.eingang.forwarding.GrpcRepresentationFile;
+
+public class GrpcRepresentationTestFactory {
+
+	public static final GrpcRepresentationFile FILE = GrpcRepresentationFileTestFactory.create();
+	public static final GrpcFileContent CONTENT = GrpcFileContentTestFactory.create();
+
+	public static GrpcRepresentation createWithFile() {
+		return GrpcRepresentation.newBuilder()
+				.setFile(FILE)
+				.build();
+	}
+
+	public static GrpcRepresentation createWithContent() {
+		return GrpcRepresentation.newBuilder()
+				.setContent(CONTENT)
+				.build();
+	}
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
new file mode 100644
index 000000000..a05ac9a84
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.forwarder;
+
+import de.ozgcloud.eingang.forwarding.GrpcAttachment;
+import de.ozgcloud.eingang.forwarding.GrpcRepresentation;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
+import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest;
+
+public class GrpcRouteForwardingRequestTestFactory {
+
+	public static final GrpcRepresentation REPRESENTATION = GrpcRepresentationTestFactory.createWithFile();
+	public static final GrpcAttachment ATTACHMENT = GrpcAttachmentTestFactory.createWithFile();
+	public static final GrpcRouteForwarding ROUTE_FORWARDING = GrpcRouteForwardingTestFactory.create();
+
+	public static GrpcRouteForwardingRequest createWithRouteForwarding() {
+		return GrpcRouteForwardingRequest.newBuilder()
+				.setRouteForwarding(ROUTE_FORWARDING)
+				.build();
+	}
+
+	public static GrpcRouteForwardingRequest createWithAttachment() {
+		return GrpcRouteForwardingRequest.newBuilder()
+				.setAttachment(ATTACHMENT)
+				.build();
+	}
+
+	public static GrpcRouteForwardingRequest createWithRepresentation() {
+		return GrpcRouteForwardingRequest.newBuilder()
+				.setRepresentation(REPRESENTATION)
+				.build();
+	}
+}
-- 
GitLab


From f7cfe07f8654d999377eed3392d97b1f72575914 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Tue, 18 Mar 2025 12:03:57 +0100
Subject: [PATCH 19/28] OZG-7573 add license header

---
 .../eingang/fim/AntragstellerMapper.java      | 23 +++++++++++++++++++
 .../de/ozgcloud/eingang/fim/FimDataUtil.java  | 23 +++++++++++++++++++
 .../eingang/fim/FimDocumentReader.java        | 23 +++++++++++++++++++
 .../ozgcloud/eingang/fim/FimSchemeHelper.java | 23 +++++++++++++++++++
 .../de/ozgcloud/eingang/fim/HeaderMapper.java | 23 +++++++++++++++++++
 .../common/errorhandling/FimException.java    | 23 +++++++++++++++++++
 .../fim/common/xml/DocumentHelper.java        | 23 +++++++++++++++++++
 .../eingang/fim/AntragstellerMapperTest.java  | 23 +++++++++++++++++++
 .../eingang/fim/AntragstellerTestFactory.java | 23 +++++++++++++++++++
 .../eingang/fim/FimBasedAdapterITCase.java    | 23 +++++++++++++++++++
 .../ozgcloud/eingang/fim/FimDataUtilTest.java | 23 +++++++++++++++++++
 .../eingang/fim/FimDocumentTestHelper.java    | 23 +++++++++++++++++++
 .../eingang/fim/FimSchemeHelperTest.java      | 23 +++++++++++++++++++
 .../ozgcloud/eingang/fim/FimServiceTest.java  | 23 +++++++++++++++++++
 .../eingang/fim/HeaderMapperTest.java         | 23 +++++++++++++++++++
 .../eingang/forwarder/FileServiceTest.java    | 23 +++++++++++++++++++
 .../ozgcloud/eingang/common/zufi/XzufiId.java | 23 +++++++++++++++++++
 .../semantik/common/FormDataTestFactory.java  | 23 +++++++++++++++++++
 18 files changed, 414 insertions(+)

diff --git a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/AntragstellerMapper.java b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/AntragstellerMapper.java
index 0c65466d7..e7b648eee 100644
--- a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/AntragstellerMapper.java
+++ b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/AntragstellerMapper.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import java.util.Map;
diff --git a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimDataUtil.java b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimDataUtil.java
index 6d5273567..b3dea53e5 100644
--- a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimDataUtil.java
+++ b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimDataUtil.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import java.util.List;
diff --git a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimDocumentReader.java b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimDocumentReader.java
index de7e3961c..8301f01ea 100644
--- a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimDocumentReader.java
+++ b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimDocumentReader.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import java.util.Collections;
diff --git a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimSchemeHelper.java b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimSchemeHelper.java
index 054589340..9fefeba4d 100644
--- a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimSchemeHelper.java
+++ b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/FimSchemeHelper.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import java.util.List;
diff --git a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/HeaderMapper.java b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/HeaderMapper.java
index 63befb663..632636410 100644
--- a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/HeaderMapper.java
+++ b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/HeaderMapper.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import java.util.List;
diff --git a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/common/errorhandling/FimException.java b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/common/errorhandling/FimException.java
index 68be9eb44..c7333daf4 100644
--- a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/common/errorhandling/FimException.java
+++ b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/common/errorhandling/FimException.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim.common.errorhandling;
 
 import de.ozgcloud.eingang.common.errorhandling.TechnicalException;
diff --git a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/common/xml/DocumentHelper.java b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/common/xml/DocumentHelper.java
index 2368cd4dd..966b2cc08 100644
--- a/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/common/xml/DocumentHelper.java
+++ b/fim-adapter/src/main/java/de/ozgcloud/eingang/fim/common/xml/DocumentHelper.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim.common.xml;
 
 import java.io.File;
diff --git a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/AntragstellerMapperTest.java b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/AntragstellerMapperTest.java
index f6daa78ea..8dd4950ec 100644
--- a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/AntragstellerMapperTest.java
+++ b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/AntragstellerMapperTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import static org.assertj.core.api.Assertions.*;
diff --git a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/AntragstellerTestFactory.java b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/AntragstellerTestFactory.java
index e72806960..6e14ba107 100644
--- a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/AntragstellerTestFactory.java
+++ b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/AntragstellerTestFactory.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import java.util.HashMap;
diff --git a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimBasedAdapterITCase.java b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimBasedAdapterITCase.java
index e32906bc4..ac669d63c 100644
--- a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimBasedAdapterITCase.java
+++ b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimBasedAdapterITCase.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import static org.assertj.core.api.Assertions.*;
diff --git a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimDataUtilTest.java b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimDataUtilTest.java
index cb48785bc..631029fe3 100644
--- a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimDataUtilTest.java
+++ b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimDataUtilTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import static org.assertj.core.api.Assertions.*;
diff --git a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimDocumentTestHelper.java b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimDocumentTestHelper.java
index 610263cc8..e5bbe51da 100644
--- a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimDocumentTestHelper.java
+++ b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimDocumentTestHelper.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import java.io.File;
diff --git a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimSchemeHelperTest.java b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimSchemeHelperTest.java
index 7553fefb3..d76848444 100644
--- a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimSchemeHelperTest.java
+++ b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimSchemeHelperTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import static org.assertj.core.api.Assertions.*;
diff --git a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimServiceTest.java b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimServiceTest.java
index 4d090842c..c9ff9d8de 100644
--- a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimServiceTest.java
+++ b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/FimServiceTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import static org.assertj.core.api.Assertions.*;
diff --git a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/HeaderMapperTest.java b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/HeaderMapperTest.java
index eef87830a..b55225451 100644
--- a/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/HeaderMapperTest.java
+++ b/fim-adapter/src/test/java/de/ozgcloud/eingang/fim/HeaderMapperTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.fim;
 
 import static org.assertj.core.api.Assertions.*;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/FileServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/FileServiceTest.java
index 795bcf585..dbf23b168 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/FileServiceTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/FileServiceTest.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.forwarder;
 
 import static org.assertj.core.api.Assertions.*;
diff --git a/router/src/main/java/de/ozgcloud/eingang/common/zufi/XzufiId.java b/router/src/main/java/de/ozgcloud/eingang/common/zufi/XzufiId.java
index ff731658b..bbcb44b94 100644
--- a/router/src/main/java/de/ozgcloud/eingang/common/zufi/XzufiId.java
+++ b/router/src/main/java/de/ozgcloud/eingang/common/zufi/XzufiId.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.common.zufi;
 
 import lombok.Builder;
diff --git a/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/FormDataTestFactory.java b/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/FormDataTestFactory.java
index 83b265781..594048a72 100644
--- a/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/FormDataTestFactory.java
+++ b/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/FormDataTestFactory.java
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.eingang.semantik.common;
 
 import java.util.HashMap;
-- 
GitLab


From 5093746ca522554be9d99e8042f5ecf53f620a14 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Thu, 20 Mar 2025 08:54:07 +0100
Subject: [PATCH 20/28] OZG-7573 only read content if EOF is false

---
 .../EingangStubReceiverStreamObserver.java    | 13 ++--
 ...EingangStubReceiverStreamObserverTest.java | 69 ++++++++++++-------
 2 files changed, 51 insertions(+), 31 deletions(-)

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index d5f1aa22a..1d355f5fd 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -156,13 +156,14 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		if (Objects.isNull(currentFile)) {
 			throw new IllegalStateException("File content received before metadata.");
 		}
-		try {
-			pipedOutput.write(content.getContent().toByteArray());
-			if (content.getIsEndOfFile()) {
-				handleEndOfFile();
+		if (content.getIsEndOfFile()) {
+			handleEndOfFile();
+		} else {
+			try {
+				pipedOutput.write(content.getContent().toByteArray());
+			} catch (IOException e) {
+				throw new TechnicalException("Error when writing file content.", e);
 			}
-		} catch (IOException e) {
-			throw new TechnicalException("Error when writing file content.", e);
 		}
 	}
 
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
index f4bacd0b2..d14d99362 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
@@ -502,42 +502,61 @@ class EingangStubReceiverStreamObserverTest {
 				setCurrentFile(incomingFile);
 			}
 
-			@Test
-			@SneakyThrows
-			void shouldWriteContentToOutputStream() {
-				observer.storeFileContent(GrpcFileContentTestFactory.create());
+			@Nested
+			class TestOnEndOfFile {
 
-				verify(pipedOutput).write(GrpcFileContentTestFactory.CONTENT);
-			}
+				private GrpcFileContent fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(true).build();
 
-			@Test
-			void shouldCallHandleEndOfFile() {
-				doNothing().when(observer).handleEndOfFile();
-				var fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(true).build();
+				@BeforeEach
+				void setUp() {
+					doNothing().when(observer).handleEndOfFile();
+				}
+
+				@Test
+				void shouldCallHandleEndOfFile() {
+					observer.storeFileContent(fileContent);
+
+					verify(observer).handleEndOfFile();
+				}
 
-				observer.storeFileContent(fileContent);
+				@Test
+				@SneakyThrows
+				void shouldNotWriteContentToOutputStream() {
+					observer.storeFileContent(fileContent);
 
-				verify(observer).handleEndOfFile();
+					verify(pipedOutput, never()).write(any());
+				}
 			}
 
-			@Test
-			void shouldNotCallHandleEndOfFile() {
-				var fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(false).build();
+			@Nested
+			class TestOnNotEndOfFile {
 
-				observer.storeFileContent(fileContent);
+				private GrpcFileContent fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(false).build();
 
-				verify(observer, never()).handleEndOfFile();
-			}
+				@Test
+				@SneakyThrows
+				void shouldWriteContentToOutputStream() {
+					observer.storeFileContent(fileContent);
 
-			@Test
-			@SneakyThrows
-			void shouldThrowTechnicalExceptionOnIOException() {
-				doThrow(new IOException()).when(pipedOutput).write(any());
-				var fileContent = GrpcFileContentTestFactory.create();
+					verify(pipedOutput).write(GrpcFileContentTestFactory.CONTENT);
+				}
 
-				assertThrows(TechnicalException.class, () -> {
+				@Test
+				void shouldNotCallHandleEndOfFile() {
 					observer.storeFileContent(fileContent);
-				});
+
+					verify(observer, never()).handleEndOfFile();
+				}
+
+				@Test
+				@SneakyThrows
+				void shouldThrowTechnicalExceptionOnIOException() {
+					doThrow(new IOException()).when(pipedOutput).write(any());
+
+					assertThrows(TechnicalException.class, () -> {
+						observer.storeFileContent(fileContent);
+					});
+				}
 			}
 		}
 	}
-- 
GitLab


From 0cf5dd0e163633de7196bf368d21d3429089530e Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Thu, 20 Mar 2025 10:57:37 +0100
Subject: [PATCH 21/28] OZG-7573 add error handler

---
 .../EingangStubReceiverStreamObserver.java    |  5 ++-
 .../forwarder/RouteForwardingGrpcService.java |  5 +++
 ...EingangStubReceiverStreamObserverTest.java | 12 ++++++
 .../RouteForwardingGrpcServiceTest.java       | 41 ++++++++++++++++---
 4 files changed, 56 insertions(+), 7 deletions(-)

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index 1d355f5fd..0d174825d 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -67,17 +67,19 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 	private final Function<InputStream, CompletableFuture<File>> fileSaver;
 	private final Consumer<FormData> formDataConsumer;
 	private final Consumer<GrpcRouteForwardingResponse> responseConsumer;
+	private final Consumer<Throwable> onErrorHandler;
 
 	@Builder
 	public EingangStubReceiverStreamObserver(RouteForwardingMapper routeForwardingMapper, IncomingFileMapper incomingFileMapper,
 			IncomingFileGroupMapper incomingFileGroupMapper, Function<InputStream, CompletableFuture<File>> fileSaver,
-			Consumer<FormData> formDataConsumer, Consumer<GrpcRouteForwardingResponse> responseConsumer) {
+			Consumer<FormData> formDataConsumer, Consumer<GrpcRouteForwardingResponse> responseConsumer, Consumer<Throwable> onErrorHandler) {
 		this.routeForwardingMapper = routeForwardingMapper;
 		this.incomingFileMapper = incomingFileMapper;
 		this.incomingFileGroupMapper = incomingFileGroupMapper;
 		this.fileSaver = fileSaver;
 		this.formDataConsumer = formDataConsumer;
 		this.responseConsumer = responseConsumer;
+		this.onErrorHandler = onErrorHandler;
 	}
 
 	private FormData formData;
@@ -202,6 +204,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		LOG.error("Error happened. Receiving stream closed.", t);
 		closeOutputPipe();
 		closeInputPipe();
+		onErrorHandler.accept(t);
 	}
 
 	void closeOutputPipe() {
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
index 322e7361f..d08d98e8e 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
@@ -52,6 +52,7 @@ public class RouteForwardingGrpcService extends RouteForwardingServiceGrpc.Route
 				.incomingFileGroupMapper(incomingFileGroupMapper)
 				.formDataConsumer(routeForwardingService::route)
 				.responseConsumer(repsonse -> respondWith(responseObserver, repsonse))
+				.onErrorHandler(error -> handleError(error, responseObserver))
 				.build();
 	}
 
@@ -64,4 +65,8 @@ public class RouteForwardingGrpcService extends RouteForwardingServiceGrpc.Route
 		responseObserver.onCompleted();
 	}
 
+	public void handleError(Throwable error, StreamObserver<GrpcRouteForwardingResponse> responseObserver) {
+		responseObserver.onError(error);
+	}
+
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
index d14d99362..fc502fe69 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
@@ -76,6 +76,8 @@ class EingangStubReceiverStreamObserverTest {
 	private Consumer<FormData> formDataConsumer;
 	@Mock
 	private Consumer<GrpcRouteForwardingResponse> responseConsumer;
+	@Mock
+	private Consumer<Throwable> onErrorHandler;
 
 	@BeforeEach
 	void setUp() {
@@ -86,6 +88,7 @@ class EingangStubReceiverStreamObserverTest {
 				.incomingFileGroupMapper(incomingFileGroupMapper)
 				.formDataConsumer(formDataConsumer)
 				.responseConsumer(responseConsumer)
+				.onErrorHandler(onErrorHandler)
 				.build());
 	}
 
@@ -827,6 +830,15 @@ class EingangStubReceiverStreamObserverTest {
 
 			verify(observer).closeInputPipe();
 		}
+
+		@Test
+		void shouldCallOnErrorHandler() {
+			var exception = new Exception();
+
+			observer.onError(exception);
+
+			verify(onErrorHandler).accept(exception);
+		}
 	}
 
 	@Nested
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
index 35ac8140c..5c94fb60b 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
@@ -122,6 +122,16 @@ class RouteForwardingGrpcServiceTest {
 			verify(service).respondWith(responseObserver, GrpcRouteForwardingResponse.getDefaultInstance());
 		}
 
+		@Test
+		void shouldSetOnErrorHandler() {
+			var error = new Throwable();
+
+			var observer = service.routeForwarding(responseObserver);
+
+			callOnErrorHandler(observer, error);
+			verify(service).handleError(error, responseObserver);
+		}
+
 		@SuppressWarnings("unchecked")
 		private void callFileSaver(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
 			var fileSaver = (Function<InputStream, CompletableFuture<File>>) ReflectionTestUtils.getField(uploadObserver, "fileSaver");
@@ -129,18 +139,15 @@ class RouteForwardingGrpcServiceTest {
 		}
 
 		private RouteForwardingMapper getRouteForwardingMapper(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
-			var routeForwardingMapper = (RouteForwardingMapper) ReflectionTestUtils.getField(uploadObserver, "routeForwardingMapper");
-			return routeForwardingMapper;
+			return (RouteForwardingMapper) ReflectionTestUtils.getField(uploadObserver, "routeForwardingMapper");
 		}
 
 		private IncomingFileMapper getIncomingFileMapper(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
-			var incomingFileMapper = (IncomingFileMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileMapper");
-			return incomingFileMapper;
+			return (IncomingFileMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileMapper");
 		}
 
 		private IncomingFileGroupMapper getIncomingFileGroupMapper(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
-			var incomingFileGroupMapper = (IncomingFileGroupMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileGroupMapper");
-			return incomingFileGroupMapper;
+			return (IncomingFileGroupMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileGroupMapper");
 		}
 
 		@SuppressWarnings("unchecked")
@@ -154,6 +161,12 @@ class RouteForwardingGrpcServiceTest {
 			var responseConsumer = (Consumer<GrpcRouteForwardingResponse>) ReflectionTestUtils.getField(uploadObserver, "responseConsumer");
 			responseConsumer.accept(response);
 		}
+
+		@SuppressWarnings("unchecked")
+		private void callOnErrorHandler(StreamObserver<GrpcRouteForwardingRequest> uploadObserver, Throwable throwable) {
+			var onErrorHandler = (Consumer<Throwable>) ReflectionTestUtils.getField(uploadObserver, "onErrorHandler");
+			onErrorHandler.accept(throwable);
+		}
 	}
 
 	@Nested
@@ -205,4 +218,20 @@ class RouteForwardingGrpcServiceTest {
 			inOrder.verify(responseObserver).onCompleted();
 		}
 	}
+
+	@Nested
+	class TestHandleError {
+
+		@Mock
+		private StreamObserver<GrpcRouteForwardingResponse> responseObserver;
+
+		@Test
+		void shouldDoNothing() {
+			var error = new Throwable();
+
+			service.handleError(error, responseObserver);
+
+			verify(responseObserver).onError(error);
+		}
+	}
 }
-- 
GitLab


From f5887f78db667568df79678e4420c72d2be88de6 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Thu, 20 Mar 2025 11:59:45 +0100
Subject: [PATCH 22/28] Revert "OZG-7573 add error handler"

This reverts commit 0cf5dd0e163633de7196bf368d21d3429089530e.
---
 .../EingangStubReceiverStreamObserver.java    |  5 +--
 .../forwarder/RouteForwardingGrpcService.java |  5 ---
 ...EingangStubReceiverStreamObserverTest.java | 12 ------
 .../RouteForwardingGrpcServiceTest.java       | 41 +++----------------
 4 files changed, 7 insertions(+), 56 deletions(-)

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index 0d174825d..1d355f5fd 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -67,19 +67,17 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 	private final Function<InputStream, CompletableFuture<File>> fileSaver;
 	private final Consumer<FormData> formDataConsumer;
 	private final Consumer<GrpcRouteForwardingResponse> responseConsumer;
-	private final Consumer<Throwable> onErrorHandler;
 
 	@Builder
 	public EingangStubReceiverStreamObserver(RouteForwardingMapper routeForwardingMapper, IncomingFileMapper incomingFileMapper,
 			IncomingFileGroupMapper incomingFileGroupMapper, Function<InputStream, CompletableFuture<File>> fileSaver,
-			Consumer<FormData> formDataConsumer, Consumer<GrpcRouteForwardingResponse> responseConsumer, Consumer<Throwable> onErrorHandler) {
+			Consumer<FormData> formDataConsumer, Consumer<GrpcRouteForwardingResponse> responseConsumer) {
 		this.routeForwardingMapper = routeForwardingMapper;
 		this.incomingFileMapper = incomingFileMapper;
 		this.incomingFileGroupMapper = incomingFileGroupMapper;
 		this.fileSaver = fileSaver;
 		this.formDataConsumer = formDataConsumer;
 		this.responseConsumer = responseConsumer;
-		this.onErrorHandler = onErrorHandler;
 	}
 
 	private FormData formData;
@@ -204,7 +202,6 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 		LOG.error("Error happened. Receiving stream closed.", t);
 		closeOutputPipe();
 		closeInputPipe();
-		onErrorHandler.accept(t);
 	}
 
 	void closeOutputPipe() {
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
index d08d98e8e..322e7361f 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcService.java
@@ -52,7 +52,6 @@ public class RouteForwardingGrpcService extends RouteForwardingServiceGrpc.Route
 				.incomingFileGroupMapper(incomingFileGroupMapper)
 				.formDataConsumer(routeForwardingService::route)
 				.responseConsumer(repsonse -> respondWith(responseObserver, repsonse))
-				.onErrorHandler(error -> handleError(error, responseObserver))
 				.build();
 	}
 
@@ -65,8 +64,4 @@ public class RouteForwardingGrpcService extends RouteForwardingServiceGrpc.Route
 		responseObserver.onCompleted();
 	}
 
-	public void handleError(Throwable error, StreamObserver<GrpcRouteForwardingResponse> responseObserver) {
-		responseObserver.onError(error);
-	}
-
 }
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
index fc502fe69..d14d99362 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
@@ -76,8 +76,6 @@ class EingangStubReceiverStreamObserverTest {
 	private Consumer<FormData> formDataConsumer;
 	@Mock
 	private Consumer<GrpcRouteForwardingResponse> responseConsumer;
-	@Mock
-	private Consumer<Throwable> onErrorHandler;
 
 	@BeforeEach
 	void setUp() {
@@ -88,7 +86,6 @@ class EingangStubReceiverStreamObserverTest {
 				.incomingFileGroupMapper(incomingFileGroupMapper)
 				.formDataConsumer(formDataConsumer)
 				.responseConsumer(responseConsumer)
-				.onErrorHandler(onErrorHandler)
 				.build());
 	}
 
@@ -830,15 +827,6 @@ class EingangStubReceiverStreamObserverTest {
 
 			verify(observer).closeInputPipe();
 		}
-
-		@Test
-		void shouldCallOnErrorHandler() {
-			var exception = new Exception();
-
-			observer.onError(exception);
-
-			verify(onErrorHandler).accept(exception);
-		}
 	}
 
 	@Nested
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
index 5c94fb60b..35ac8140c 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/RouteForwardingGrpcServiceTest.java
@@ -122,16 +122,6 @@ class RouteForwardingGrpcServiceTest {
 			verify(service).respondWith(responseObserver, GrpcRouteForwardingResponse.getDefaultInstance());
 		}
 
-		@Test
-		void shouldSetOnErrorHandler() {
-			var error = new Throwable();
-
-			var observer = service.routeForwarding(responseObserver);
-
-			callOnErrorHandler(observer, error);
-			verify(service).handleError(error, responseObserver);
-		}
-
 		@SuppressWarnings("unchecked")
 		private void callFileSaver(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
 			var fileSaver = (Function<InputStream, CompletableFuture<File>>) ReflectionTestUtils.getField(uploadObserver, "fileSaver");
@@ -139,15 +129,18 @@ class RouteForwardingGrpcServiceTest {
 		}
 
 		private RouteForwardingMapper getRouteForwardingMapper(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
-			return (RouteForwardingMapper) ReflectionTestUtils.getField(uploadObserver, "routeForwardingMapper");
+			var routeForwardingMapper = (RouteForwardingMapper) ReflectionTestUtils.getField(uploadObserver, "routeForwardingMapper");
+			return routeForwardingMapper;
 		}
 
 		private IncomingFileMapper getIncomingFileMapper(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
-			return (IncomingFileMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileMapper");
+			var incomingFileMapper = (IncomingFileMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileMapper");
+			return incomingFileMapper;
 		}
 
 		private IncomingFileGroupMapper getIncomingFileGroupMapper(StreamObserver<GrpcRouteForwardingRequest> uploadObserver) {
-			return (IncomingFileGroupMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileGroupMapper");
+			var incomingFileGroupMapper = (IncomingFileGroupMapper) ReflectionTestUtils.getField(uploadObserver, "incomingFileGroupMapper");
+			return incomingFileGroupMapper;
 		}
 
 		@SuppressWarnings("unchecked")
@@ -161,12 +154,6 @@ class RouteForwardingGrpcServiceTest {
 			var responseConsumer = (Consumer<GrpcRouteForwardingResponse>) ReflectionTestUtils.getField(uploadObserver, "responseConsumer");
 			responseConsumer.accept(response);
 		}
-
-		@SuppressWarnings("unchecked")
-		private void callOnErrorHandler(StreamObserver<GrpcRouteForwardingRequest> uploadObserver, Throwable throwable) {
-			var onErrorHandler = (Consumer<Throwable>) ReflectionTestUtils.getField(uploadObserver, "onErrorHandler");
-			onErrorHandler.accept(throwable);
-		}
 	}
 
 	@Nested
@@ -218,20 +205,4 @@ class RouteForwardingGrpcServiceTest {
 			inOrder.verify(responseObserver).onCompleted();
 		}
 	}
-
-	@Nested
-	class TestHandleError {
-
-		@Mock
-		private StreamObserver<GrpcRouteForwardingResponse> responseObserver;
-
-		@Test
-		void shouldDoNothing() {
-			var error = new Throwable();
-
-			service.handleError(error, responseObserver);
-
-			verify(responseObserver).onError(error);
-		}
-	}
 }
-- 
GitLab


From 6fd814680f5b1f49ad9a451918b9f0819266b20c Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Thu, 20 Mar 2025 14:23:27 +0100
Subject: [PATCH 23/28] Ozg-7573 apply code review

---
 .../EingangStubReceiverStreamObserver.java    | 17 +++++++-------
 ...EingangStubReceiverStreamObserverTest.java | 22 +++++++++----------
 2 files changed, 19 insertions(+), 20 deletions(-)

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index 1d355f5fd..fd810664e 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -57,7 +57,7 @@ import lombok.Builder;
 import lombok.extern.log4j.Log4j2;
 
 @Log4j2
-public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwardingRequest> {
+class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwardingRequest> {
 
 	private static final int CHUNK_SIZE = 1024 * 64;
 	private static final long TIMEOUT_MINUTES = 10;
@@ -81,11 +81,11 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 	}
 
 	private FormData formData;
-	private List<IncomingFile> representations = new ArrayList<>();
-	private Map<String, List<IncomingFile>> attachments = new HashMap<>();
+	private final List<IncomingFile> representations = new ArrayList<>();
+	private final Map<String, List<IncomingFile>> attachments = new HashMap<>();
 
 	private IncomingFile currentFile;
-	private Optional<String> groupName = Optional.empty();
+	private String groupName;
 	private PipedOutputStream pipedOutput;
 	private PipedInputStream pipedInput;
 	private CompletableFuture<File> receivingFileContent;
@@ -113,7 +113,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 	void handleAttachment(GrpcAttachment attachment) {
 		if (attachment.hasFile()) {
 			setCurrentMetadata(incomingFileMapper.fromGrpcAttachmentFile(attachment.getFile()));
-			groupName = Optional.of(attachment.getFile().getGroupName());
+			groupName = attachment.getFile().getGroupName();
 		} else {
 			handleFileContent(attachment.getContent());
 		}
@@ -130,7 +130,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 
 	void setCurrentMetadata(IncomingFile metaData) {
 		if (Objects.nonNull(currentFile)) {
-			throw new IllegalStateException("Received additional file before previos file reached the end.");
+			throw new IllegalStateException("Received additional file before previous file reached the end.");
 		}
 		currentFile = metaData;
 	}
@@ -170,7 +170,8 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 	void handleEndOfFile() {
 		closeOutputPipe();
 		var completedIncomingFile = currentFile.toBuilder().file(getSavedFileContent()).build();
-		groupName.map(group -> attachments.computeIfAbsent(group, s -> new ArrayList<IncomingFile>()))
+		Optional.ofNullable(groupName)
+				.map(group -> attachments.computeIfAbsent(group, s -> new ArrayList<>()))
 				.orElse(representations)
 				.add(completedIncomingFile);
 		resetFileReceiving();
@@ -191,7 +192,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou
 
 	void resetFileReceiving() {
 		currentFile = null;
-		groupName = Optional.empty();
+		groupName = null;
 		pipedOutput = null;
 		pipedInput = null;
 		receivingFileContent = null;
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
index d14d99362..04d21376b 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
@@ -35,7 +35,6 @@ import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
@@ -235,7 +234,7 @@ class EingangStubReceiverStreamObserverTest {
 			void shouldSetGroupName() {
 				observer.handleAttachment(attachmentWithFile);
 
-				assertThat(getGroupName()).contains(GrpcAttachmentFileTestFactory.GROUP_NAME);
+				assertThat(getGroupName()).isEqualTo(GrpcAttachmentFileTestFactory.GROUP_NAME);
 			}
 
 			@Test
@@ -274,7 +273,7 @@ class EingangStubReceiverStreamObserverTest {
 			void shouldNotSetGroupName() {
 				observer.handleAttachment(attachmentWithContent);
 
-				assertThat(getGroupName()).isEmpty();
+				assertThat(getGroupName()).isNull();
 			}
 		}
 	}
@@ -584,11 +583,11 @@ class EingangStubReceiverStreamObserverTest {
 		}
 
 		@Nested
-		class TestOnGroupNameEmpty {
+		class TestOnGroupNameNull {
 
 			@BeforeEach
 			void setUp() {
-				setGroupName(Optional.empty());
+				setGroupName(null);
 			}
 
 			@Test
@@ -606,7 +605,7 @@ class EingangStubReceiverStreamObserverTest {
 
 			@BeforeEach
 			void setUp() {
-				setGroupName(Optional.of(GrpcAttachmentFileTestFactory.GROUP_NAME));
+				setGroupName(GrpcAttachmentFileTestFactory.GROUP_NAME);
 			}
 
 			@Test
@@ -763,7 +762,7 @@ class EingangStubReceiverStreamObserverTest {
 		@BeforeEach
 		void setUp() {
 			setCurrentFile(IncomingFileTestFactory.create());
-			setGroupName(Optional.of(GrpcAttachmentFileTestFactory.GROUP_NAME));
+			setGroupName(GrpcAttachmentFileTestFactory.GROUP_NAME);
 			setPipedOutput(mock(PipedOutputStream.class));
 			setPipedOutput(mock(PipedOutputStream.class));
 			setFileContent(CompletableFuture.completedFuture(mock(File.class)));
@@ -780,7 +779,7 @@ class EingangStubReceiverStreamObserverTest {
 		void shouldResetGroupName() {
 			observer.resetFileReceiving();
 
-			assertThat(getGroupName()).isEmpty();
+			assertThat(getGroupName()).isNull();
 		}
 
 		@Test
@@ -980,12 +979,11 @@ class EingangStubReceiverStreamObserverTest {
 		ReflectionTestUtils.setField(observer, "formData", formData);
 	}
 
-	@SuppressWarnings("unchecked")
-	private Optional<String> getGroupName() {
-		return (Optional<String>) ReflectionTestUtils.getField(observer, "groupName");
+	private String getGroupName() {
+		return (String) ReflectionTestUtils.getField(observer, "groupName");
 	}
 
-	private void setGroupName(Optional<String> groupName) {
+	private void setGroupName(String groupName) {
 		ReflectionTestUtils.setField(observer, "groupName", groupName);
 	}
 
-- 
GitLab


From 9b808646c3a89ceedb6fe3df822719d99f2bb14c Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Thu, 20 Mar 2025 14:35:41 +0100
Subject: [PATCH 24/28] OZG-7573 refactor handleEndOfFile

---
 .../EingangStubReceiverStreamObserver.java     | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index fd810664e..ec9080a75 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -33,7 +33,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -170,13 +169,22 @@ class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwa
 	void handleEndOfFile() {
 		closeOutputPipe();
 		var completedIncomingFile = currentFile.toBuilder().file(getSavedFileContent()).build();
-		Optional.ofNullable(groupName)
-				.map(group -> attachments.computeIfAbsent(group, s -> new ArrayList<>()))
-				.orElse(representations)
-				.add(completedIncomingFile);
+		if (Objects.isNull(groupName)) {
+			addAsRepresentation(completedIncomingFile);
+		} else {
+			addAsAttachment(completedIncomingFile);
+		}
 		resetFileReceiving();
 	}
 
+	private void addAsRepresentation(IncomingFile completedIncomingFile) {
+		representations.add(completedIncomingFile);
+	}
+
+	private void addAsAttachment(IncomingFile completedIncomingFile) {
+		attachments.computeIfAbsent(groupName, s -> new ArrayList<>()).add(completedIncomingFile);
+	}
+
 	File getSavedFileContent() {
 		try {
 			return receivingFileContent.get(TIMEOUT_MINUTES, TimeUnit.MINUTES);
-- 
GitLab


From 7dc12961d47a0816ce9eca12437aafbb85b4a0ab Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Fri, 21 Mar 2025 08:26:38 +0100
Subject: [PATCH 25/28] OZG-7573 refactor EingangStubReceiverStreamObserver and
 create ContentCollector

---
 .../formdata/IncomingFileTestFactory.java     |   4 +-
 .../eingang/forwarder/ContentCollector.java   | 112 +++
 .../EingangStubReceiverStreamObserver.java    | 125 +---
 .../forwarder/ContentCollectorTest.java       | 484 +++++++++++++
 ...EingangStubReceiverStreamObserverTest.java | 657 +++---------------
 5 files changed, 735 insertions(+), 647 deletions(-)
 create mode 100644 forwarder/src/main/java/de/ozgcloud/eingang/forwarder/ContentCollector.java
 create mode 100644 forwarder/src/test/java/de/ozgcloud/eingang/forwarder/ContentCollectorTest.java

diff --git a/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileTestFactory.java b/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileTestFactory.java
index f377a61cd..85a108436 100644
--- a/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileTestFactory.java
+++ b/common/src/test/java/de/ozgcloud/eingang/common/formdata/IncomingFileTestFactory.java
@@ -23,6 +23,7 @@
  */
 package de.ozgcloud.eingang.common.formdata;
 
+import java.io.File;
 import java.util.UUID;
 
 import org.springframework.http.MediaType;
@@ -41,6 +42,7 @@ public class IncomingFileTestFactory {
 	public static final String PDF_CONTENT_TYPE = MediaType.APPLICATION_PDF_VALUE;
 	public static final String JSON_CONTENT_TYPE = MediaType.APPLICATION_JSON_VALUE;
 	public static final byte[] CONTENT = "TESTCONTENT1".getBytes();
+	public static final File FILE = TempFileUtils.writeTmpFile(CONTENT);
 	public static final long SIZE = 12;
 
 	public static IncomingFile create() {
@@ -53,7 +55,7 @@ public class IncomingFileTestFactory {
 				.vendorId(VENDOR_ID)
 				.name(NAME)
 				.contentType(CONTENT_TYPE)
-				.file(TempFileUtils.writeTmpFile(CONTENT))
+				.file(FILE)
 				.size(SIZE);
 	}
 
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/ContentCollector.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/ContentCollector.java
new file mode 100644
index 000000000..5099f2385
--- /dev/null
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/ContentCollector.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.forwarder;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+
+import org.apache.commons.io.IOUtils;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.eingang.common.formdata.IncomingFile;
+import de.ozgcloud.eingang.forwarding.GrpcFileContent;
+import lombok.Builder;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+class ContentCollector {
+	private static final int CHUNK_SIZE = 1024 * 64;
+	private static final long TIMEOUT_MINUTES = 10;
+
+	private final IncomingFile incomingFile;
+
+	private final PipedOutputStream pipedOutput;
+	private final PipedInputStream pipedInput;
+	private final CompletableFuture<File> receivingFileContent;
+
+	@Builder
+	private ContentCollector(Function<InputStream, CompletableFuture<File>> fileSaver, IncomingFile incomingFile) {
+		this.incomingFile = incomingFile;
+		try {
+			pipedInput = new PipedInputStream(CHUNK_SIZE);
+			pipedOutput = new PipedOutputStream(pipedInput);
+			receivingFileContent = fileSaver.apply(pipedInput);
+		} catch (IOException e) {
+			throw new TechnicalException("Upload initialization failed", e);
+		}
+	}
+
+	public Optional<IncomingFile> collect(GrpcFileContent fileContent) {
+		if (fileContent.getIsEndOfFile()) {
+			return Optional.of(handleEndOfFile());
+		}
+		try {
+			pipedOutput.write(fileContent.getContent().toByteArray());
+		} catch (IOException e) {
+			throw new TechnicalException("Error when writing file content.", e);
+		}
+		return Optional.empty();
+	}
+
+	IncomingFile handleEndOfFile() {
+		closeOutputPipe();
+		return incomingFile.toBuilder().file(getSavedFileContent()).build();
+	}
+
+	File getSavedFileContent() {
+		try {
+			return receivingFileContent.get(TIMEOUT_MINUTES, TimeUnit.MINUTES);
+		} catch (ExecutionException | TimeoutException e) {
+			throw new TechnicalException("Receiving file failed.", e);
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+			throw new TechnicalException("Upload was interrupted.", e);
+		} finally {
+			closeInputPipe();
+		}
+	}
+
+	public void close() {
+		closeOutputPipe();
+		closeInputPipe();
+	}
+
+	void closeOutputPipe() {
+		IOUtils.closeQuietly(pipedOutput, e -> LOG.error("Cannot close output stream.", e));
+	}
+
+	void closeInputPipe() {
+		IOUtils.closeQuietly(pipedInput, e -> LOG.error("Cannot close input stream.", e));
+	}
+
+}
diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index ec9080a75..45f253538 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -24,29 +24,19 @@
 package de.ozgcloud.eingang.forwarder;
 
 import java.io.File;
-import java.io.IOException;
 import java.io.InputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-import org.apache.commons.io.IOUtils;
-
-import de.ozgcloud.common.errorhandling.TechnicalException;
 import de.ozgcloud.eingang.common.formdata.FormData;
 import de.ozgcloud.eingang.common.formdata.IncomingFile;
 import de.ozgcloud.eingang.forwarding.GrpcAttachment;
-import de.ozgcloud.eingang.forwarding.GrpcFileContent;
 import de.ozgcloud.eingang.forwarding.GrpcRepresentation;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest;
@@ -58,8 +48,6 @@ import lombok.extern.log4j.Log4j2;
 @Log4j2
 class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwardingRequest> {
 
-	private static final int CHUNK_SIZE = 1024 * 64;
-	private static final long TIMEOUT_MINUTES = 10;
 	private final RouteForwardingMapper routeForwardingMapper;
 	private final IncomingFileMapper incomingFileMapper;
 	private final IncomingFileGroupMapper incomingFileGroupMapper;
@@ -83,11 +71,9 @@ class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwa
 	private final List<IncomingFile> representations = new ArrayList<>();
 	private final Map<String, List<IncomingFile>> attachments = new HashMap<>();
 
-	private IncomingFile currentFile;
 	private String groupName;
-	private PipedOutputStream pipedOutput;
-	private PipedInputStream pipedInput;
-	private CompletableFuture<File> receivingFileContent;
+	private ContentCollector attachmentCollector;
+	private ContentCollector representationCollector;
 
 	@Override
 	public synchronized void onNext(GrpcRouteForwardingRequest request) {
@@ -111,114 +97,49 @@ class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwa
 
 	void handleAttachment(GrpcAttachment attachment) {
 		if (attachment.hasFile()) {
-			setCurrentMetadata(incomingFileMapper.fromGrpcAttachmentFile(attachment.getFile()));
+			attachmentCollector = buildContentCollector(incomingFileMapper.fromGrpcAttachmentFile(attachment.getFile()));
 			groupName = attachment.getFile().getGroupName();
 		} else {
-			handleFileContent(attachment.getContent());
+			if (Objects.isNull(attachmentCollector)) {
+				throw new IllegalStateException("File content received before metadata.");
+			}
+			attachmentCollector.collect(attachment.getContent()).ifPresent(this::addAsAttachment);
 		}
 	}
 
 	void handleRepresentation(GrpcRepresentation representation) {
 		if (representation.hasFile()) {
-			setCurrentMetadata(incomingFileMapper.fromGrpcRepresentationFile(representation.getFile()));
+			representationCollector = buildContentCollector(incomingFileMapper.fromGrpcRepresentationFile(representation.getFile()));
 		} else {
-			handleFileContent(representation.getContent());
-		}
-
-	}
-
-	void setCurrentMetadata(IncomingFile metaData) {
-		if (Objects.nonNull(currentFile)) {
-			throw new IllegalStateException("Received additional file before previous file reached the end.");
-		}
-		currentFile = metaData;
-	}
-
-	void handleFileContent(GrpcFileContent fileContent) {
-		if (Objects.isNull(receivingFileContent)) {
-			initContentReceiving();
-		}
-		storeFileContent(fileContent);
-	}
-
-	void initContentReceiving() {
-		try {
-			pipedInput = new PipedInputStream(CHUNK_SIZE);
-			pipedOutput = new PipedOutputStream(pipedInput);
-			receivingFileContent = fileSaver.apply(pipedInput);
-		} catch (IOException e) {
-			throw new TechnicalException("Upload initialization failed", e);
-		}
-	}
-
-	void storeFileContent(GrpcFileContent content) {
-		if (Objects.isNull(currentFile)) {
-			throw new IllegalStateException("File content received before metadata.");
-		}
-		if (content.getIsEndOfFile()) {
-			handleEndOfFile();
-		} else {
-			try {
-				pipedOutput.write(content.getContent().toByteArray());
-			} catch (IOException e) {
-				throw new TechnicalException("Error when writing file content.", e);
+			if (Objects.isNull(representationCollector)) {
+				throw new IllegalStateException("File content received before metadata.");
 			}
+			representationCollector.collect(representation.getContent()).ifPresent(this::addAsRepresentation);
 		}
 	}
 
-	void handleEndOfFile() {
-		closeOutputPipe();
-		var completedIncomingFile = currentFile.toBuilder().file(getSavedFileContent()).build();
-		if (Objects.isNull(groupName)) {
-			addAsRepresentation(completedIncomingFile);
-		} else {
-			addAsAttachment(completedIncomingFile);
-		}
-		resetFileReceiving();
-	}
-
-	private void addAsRepresentation(IncomingFile completedIncomingFile) {
-		representations.add(completedIncomingFile);
+	private ContentCollector buildContentCollector(IncomingFile incomingFile) {
+		return ContentCollector.builder()
+				.fileSaver(fileSaver)
+				.incomingFile(incomingFile)
+				.build();
 	}
 
-	private void addAsAttachment(IncomingFile completedIncomingFile) {
+	void addAsAttachment(IncomingFile completedIncomingFile) {
 		attachments.computeIfAbsent(groupName, s -> new ArrayList<>()).add(completedIncomingFile);
+		attachmentCollector = null;
 	}
 
-	File getSavedFileContent() {
-		try {
-			return receivingFileContent.get(TIMEOUT_MINUTES, TimeUnit.MINUTES);
-		} catch (ExecutionException | TimeoutException e) {
-			throw new TechnicalException("Receiving file failed.", e);
-		} catch (InterruptedException e) {
-			Thread.currentThread().interrupt();
-			throw new TechnicalException("Upload was interrupted.", e);
-		} finally {
-			closeInputPipe();
-		}
-	}
-
-	void resetFileReceiving() {
-		currentFile = null;
-		groupName = null;
-		pipedOutput = null;
-		pipedInput = null;
-		receivingFileContent = null;
+	void addAsRepresentation(IncomingFile completedIncomingFile) {
+		representations.add(completedIncomingFile);
+		representationCollector = null;
 	}
 
 	@Override
 	public synchronized void onError(Throwable t) {
 		LOG.error("Error happened. Receiving stream closed.", t);
-		closeOutputPipe();
-		closeInputPipe();
-	}
-
-	void closeOutputPipe() {
-		IOUtils.closeQuietly(pipedOutput, e -> LOG.error("Cannot close output stream.", e));
-	}
-
-	void closeInputPipe() {
-		IOUtils.closeQuietly(pipedInput, e -> LOG.error("Cannot close input stream.", e));
+		attachmentCollector.close();
+		representationCollector.close();
 	}
 
 	@Override
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/ContentCollectorTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/ContentCollectorTest.java
new file mode 100644
index 000000000..4385eb7ef
--- /dev/null
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/ContentCollectorTest.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.eingang.forwarder;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.eingang.common.formdata.IncomingFile;
+import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
+import de.ozgcloud.eingang.forwarding.GrpcFileContent;
+import lombok.SneakyThrows;
+
+class ContentCollectorTest {
+
+	private ContentCollector contentCollector;
+	@Mock
+	private Function<InputStream, CompletableFuture<File>> fileSaver;
+	@Mock
+	private CompletableFuture<File> fileContentFuture;
+	private final IncomingFile incomingFile = IncomingFileTestFactory.createBuilder().file(null).build();
+
+	void setUpContentCollector() {
+		when(fileSaver.apply(any())).thenReturn(fileContentFuture);
+		contentCollector = spy(ContentCollector.builder().fileSaver(fileSaver).incomingFile(incomingFile).build());
+	}
+
+	@Nested
+	class TestBuilding {
+
+		private final byte[] content = new byte[] { 1, 2, 3 };
+
+		@Test
+		void shouldSetIncomingFile() {
+			setUpContentCollector();
+
+			assertThat(getIncomingFile()).isSameAs(incomingFile);
+		}
+
+		@Test
+		void shouldCreateInputStream() {
+			setUpContentCollector();
+
+			assertThat(getPipedInput()).isNotNull();
+		}
+
+		@Test
+		void shouldCreateOutputStream() {
+			setUpContentCollector();
+
+			assertThat(getPipedOutput()).isNotNull();
+		}
+
+		@Test
+		void shouldCreateConnectedStreams() {
+			setUpContentCollector();
+
+			verifyStreamSetUp();
+		}
+
+		@SneakyThrows
+		private void verifyStreamSetUp() {
+			var pipedInput = getPipedInput();
+			var pipedOutput = getPipedOutput();
+			pipedOutput.write(content);
+			pipedOutput.close();
+			var readBytes = pipedInput.readAllBytes();
+			assertThat(readBytes).isEqualTo(content);
+		}
+
+		@Test
+		void shouldCallFileSaver() {
+			setUpContentCollector();
+
+			verify(fileSaver).apply(getPipedInput());
+		}
+
+		@Test
+		void shouldSetReceivingFileContent() {
+			setUpContentCollector();
+
+			assertThat(getFileContent()).isSameAs(fileContentFuture);
+		}
+	}
+
+	@Nested
+	class TestCollect {
+
+		@Mock
+		private PipedOutputStream pipedOutput;
+
+		@BeforeEach
+		void setUp() {
+			setUpContentCollector();
+			setPipedOutput(pipedOutput);
+		}
+
+		@Nested
+		class TestOnEndOfFile {
+
+			private final GrpcFileContent fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(true).build();
+			private final IncomingFile completedIncomingFile = IncomingFileTestFactory.create();
+
+			@BeforeEach
+			void setUp() {
+				doReturn(completedIncomingFile).when(contentCollector).handleEndOfFile();
+			}
+
+			@Test
+			void shouldCallHandleEndOfFile() {
+				contentCollector.collect(fileContent);
+
+				verify(contentCollector).handleEndOfFile();
+			}
+
+			@Test
+			@SneakyThrows
+			void shouldNotWriteContentToOutputStream() {
+				contentCollector.collect(fileContent);
+
+				verify(pipedOutput, never()).write(any());
+			}
+
+			@Test
+			void shouldReturnCompletedIncomingFile() {
+				var result = contentCollector.collect(fileContent);
+
+				assertThat(result).contains(completedIncomingFile);
+			}
+		}
+
+		@Nested
+		class TestOnNotEndOfFile {
+
+			private final GrpcFileContent fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(false).build();
+
+			@Test
+			@SneakyThrows
+			void shouldWriteContentToOutputStream() {
+				contentCollector.collect(fileContent);
+
+				verify(pipedOutput).write(GrpcFileContentTestFactory.CONTENT);
+			}
+
+			@Test
+			void shouldNotCallHandleEndOfFile() {
+				contentCollector.collect(fileContent);
+
+				verify(contentCollector, never()).handleEndOfFile();
+			}
+
+			@Test
+			void shouldReturnEmpty() {
+				var result = contentCollector.collect(fileContent);
+
+				assertThat(result).isEmpty();
+			}
+
+			@Test
+			@SneakyThrows
+			void shouldThrowTechnicalExceptionOnIOException() {
+				doThrow(new IOException()).when(pipedOutput).write(any());
+
+				assertThrows(TechnicalException.class, () -> {
+					contentCollector.collect(fileContent);
+				});
+			}
+		}
+	}
+
+	@Nested
+	class TestHandleEndOfFile {
+
+		@BeforeEach
+		void setUp() {
+			setUpContentCollector();
+			doNothing().when(contentCollector).closeOutputPipe();
+			doReturn(IncomingFileTestFactory.FILE).when(contentCollector).getSavedFileContent();
+		}
+
+		@Test
+		void shouldCallCloseOutputPipe() {
+			contentCollector.handleEndOfFile();
+
+			verify(contentCollector).closeOutputPipe();
+		}
+
+		@Test
+		void shouldCallGetSavedFileContent() {
+			contentCollector.handleEndOfFile();
+
+			verify(contentCollector).getSavedFileContent();
+		}
+
+		@Test
+		void shouldReturnIncomingFileWithSavedFile() {
+			var returnedIncomingFile = contentCollector.handleEndOfFile();
+
+			assertThat(returnedIncomingFile).usingRecursiveComparison().isEqualTo(IncomingFileTestFactory.create());
+		}
+	}
+
+	@Nested
+	class TestGetSavedFileContent {
+		@BeforeEach
+		void setUp() {
+			setUpContentCollector();
+			doNothing().when(contentCollector).closeInputPipe();
+		}
+
+		@Nested
+		class TestOnNoExceptions {
+			@Mock
+			private File fileContent;
+
+			@BeforeEach
+			@SneakyThrows
+			void setUp() {
+				when(fileContentFuture.get(anyLong(), any())).thenReturn(fileContent);
+			}
+
+			@Test
+			void shouldReturnFile() {
+				var savedFileContent = contentCollector.getSavedFileContent();
+
+				assertThat(savedFileContent).isSameAs(fileContent);
+			}
+
+			@Test
+			void shouldCallCloseInputPipe() {
+				contentCollector.getSavedFileContent();
+
+				verify(contentCollector).closeInputPipe();
+			}
+		}
+
+		@Nested
+		class TestOnExecutionException {
+
+			@BeforeEach
+			void setUp() {
+				setFileContent(CompletableFuture.failedFuture(new Exception()));
+			}
+
+			@Test
+			void shouldThrowTechnicalException() {
+				assertThrows(TechnicalException.class, () -> contentCollector.getSavedFileContent());
+			}
+
+			@Test
+			void shouldCallCloseInputPipe() {
+				try {
+					contentCollector.getSavedFileContent();
+				} catch (TechnicalException e) {
+					// expected
+				}
+
+				verify(contentCollector).closeInputPipe();
+			}
+		}
+
+		@Nested
+		class TestOnTimeoutException {
+
+			@Mock
+			private CompletableFuture<File> fileFuture;
+
+			@BeforeEach
+			@SneakyThrows
+			void setUp() {
+				setFileContent(fileFuture);
+				when(fileFuture.get(anyLong(), any())).thenThrow(new TimeoutException());
+			}
+
+			@Test
+			void shouldThrowTechnicalException() {
+				assertThrows(TechnicalException.class, () -> contentCollector.getSavedFileContent());
+			}
+
+			@Test
+			void shouldCallCloseInputPipe() {
+				try {
+					contentCollector.getSavedFileContent();
+				} catch (TechnicalException e) {
+					// expected
+				}
+
+				verify(contentCollector).closeInputPipe();
+			}
+		}
+
+		@Nested
+		class TestOnInterruptedException {
+
+			@Mock
+			private CompletableFuture<File> fileFuture;
+
+			@BeforeEach
+			@SneakyThrows
+			void setUp() {
+				setFileContent(fileFuture);
+				when(fileFuture.get(anyLong(), any())).thenThrow(new InterruptedException());
+			}
+
+			@Test
+			void shouldThrowTechnicalException() {
+				assertThrows(TechnicalException.class, () -> contentCollector.getSavedFileContent());
+			}
+
+			@Test
+			void shouldInterruptCurrentThread() {
+				try {
+					contentCollector.getSavedFileContent();
+				} catch (TechnicalException e) {
+					// expected
+				}
+
+				assertThat(Thread.currentThread().isInterrupted()).isTrue();
+			}
+
+			@Test
+			void shouldCallCloseInputPipe() {
+				try {
+					contentCollector.getSavedFileContent();
+				} catch (TechnicalException e) {
+					// expected
+				}
+
+				verify(contentCollector).closeInputPipe();
+			}
+		}
+	}
+
+	@Nested
+	class TestClose {
+
+		@BeforeEach
+		void setUp() {
+			setUpContentCollector();
+			doNothing().when(contentCollector).closeOutputPipe();
+			doNothing().when(contentCollector).closeInputPipe();
+		}
+
+		@Test
+		void shouldCallCloseOutputPipe() {
+			contentCollector.close();
+
+			verify(contentCollector).closeOutputPipe();
+		}
+
+		@Test
+		void shouldCallCloseInputPipe() {
+			contentCollector.close();
+
+			verify(contentCollector).closeInputPipe();
+		}
+	}
+
+	@Nested
+	class TestCloseOutputPipe {
+
+		@Mock
+		private PipedOutputStream pipedOutput;
+
+		@BeforeEach
+		void setUp() {
+			setUpContentCollector();
+			setPipedOutput(pipedOutput);
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldClosePipedOutput() {
+			contentCollector.closeOutputPipe();
+
+			verify(pipedOutput).close();
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldNotThrowException() {
+			doThrow(IOException.class).when(pipedOutput).close();
+
+			assertDoesNotThrow(() -> contentCollector.closeOutputPipe());
+		}
+	}
+
+	@Nested
+	class TestCloseInputPipe {
+
+		@Mock
+		private PipedInputStream pipedInput;
+
+		@BeforeEach
+		void setUp() {
+			setUpContentCollector();
+			setPipedInput(pipedInput);
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldClosePipedInput() {
+			contentCollector.closeInputPipe();
+
+			verify(pipedInput).close();
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldNotThrowException() {
+			doThrow(IOException.class).when(pipedInput).close();
+
+			assertDoesNotThrow(() -> contentCollector.closeInputPipe());
+		}
+	}
+
+	@SuppressWarnings("unchecked")
+	private CompletableFuture<File> getFileContent() {
+		return (CompletableFuture<File>) ReflectionTestUtils.getField(contentCollector, "receivingFileContent");
+	}
+
+	private void setFileContent(CompletableFuture<File> fileContent) {
+		ReflectionTestUtils.setField(contentCollector, "receivingFileContent", fileContent);
+	}
+
+	private PipedInputStream getPipedInput() {
+		return (PipedInputStream) ReflectionTestUtils.getField(contentCollector, "pipedInput");
+	}
+
+	private void setPipedInput(PipedInputStream pipedInput) {
+		ReflectionTestUtils.setField(contentCollector, "pipedInput", pipedInput);
+	}
+
+	private PipedOutputStream getPipedOutput() {
+		return (PipedOutputStream) ReflectionTestUtils.getField(contentCollector, "pipedOutput");
+	}
+
+	private void setPipedOutput(PipedOutputStream pipedOutput) {
+		ReflectionTestUtils.setField(contentCollector, "pipedOutput", pipedOutput);
+	}
+
+	private IncomingFile getIncomingFile() {
+		return (IncomingFile) ReflectionTestUtils.getField(contentCollector, "incomingFile");
+	}
+
+}
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
index 04d21376b..f312ec86f 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
@@ -29,14 +29,11 @@ import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
 import java.io.File;
-import java.io.IOException;
 import java.io.InputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -46,18 +43,15 @@ import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
 import org.springframework.test.util.ReflectionTestUtils;
 
-import de.ozgcloud.common.errorhandling.TechnicalException;
 import de.ozgcloud.eingang.common.formdata.FormData;
 import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
 import de.ozgcloud.eingang.common.formdata.IncomingFile;
 import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory;
 import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory;
 import de.ozgcloud.eingang.forwarding.GrpcAttachment;
-import de.ozgcloud.eingang.forwarding.GrpcFileContent;
 import de.ozgcloud.eingang.forwarding.GrpcRepresentation;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding;
 import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingResponse;
-import lombok.SneakyThrows;
 
 class EingangStubReceiverStreamObserverTest {
 
@@ -204,6 +198,14 @@ class EingangStubReceiverStreamObserverTest {
 	@Nested
 	class TestHandleAttachment {
 
+		@Mock
+		private ContentCollector attachmentCollector;
+
+		@BeforeEach
+		void setUp() {
+			setAttachmentCollector(attachmentCollector);
+		}
+
 		@Nested
 		class TestWithFile {
 
@@ -212,7 +214,6 @@ class EingangStubReceiverStreamObserverTest {
 
 			@BeforeEach
 			void mock() {
-				doNothing().when(observer).setCurrentMetadata(any());
 				when(incomingFileMapper.fromGrpcAttachmentFile(any())).thenReturn(incomingFile);
 			}
 
@@ -224,10 +225,13 @@ class EingangStubReceiverStreamObserverTest {
 			}
 
 			@Test
-			void shouldCallSetCurrentMetadata() {
+			void shouldSetAttachmentCollector() {
+				var expectedContentCollector = ContentCollector.builder().fileSaver(fileSaver).incomingFile(incomingFile).build();
+
 				observer.handleAttachment(attachmentWithFile);
 
-				verify(observer).setCurrentMetadata(incomingFile);
+				assertThat(getAttachmentCollector()).usingRecursiveComparison().ignoringFields("pipedInput", "pipedOutput")
+						.isEqualTo(expectedContentCollector);
 			}
 
 			@Test
@@ -238,10 +242,10 @@ class EingangStubReceiverStreamObserverTest {
 			}
 
 			@Test
-			void shouldNotCallHandleFileContent() {
+			void shouldNotCollectContent() {
 				observer.handleAttachment(attachmentWithFile);
 
-				verify(observer, never()).handleFileContent(any());
+				verify(attachmentCollector, never()).collect(any());
 			}
 		}
 
@@ -249,17 +253,13 @@ class EingangStubReceiverStreamObserverTest {
 		class TestWithContent {
 
 			private final GrpcAttachment attachmentWithContent = GrpcAttachmentTestFactory.createWithContent();
-
-			@BeforeEach
-			void mock() {
-				doNothing().when(observer).handleFileContent(any());
-			}
+			private final IncomingFile incomingFile = IncomingFileTestFactory.create();
 
 			@Test
-			void shouldCallHandleFileContent() {
+			void shouldCollectContent() {
 				observer.handleAttachment(attachmentWithContent);
 
-				verify(observer).handleFileContent(GrpcAttachmentTestFactory.CONTENT);
+				verify(attachmentCollector).collect(GrpcAttachmentTestFactory.CONTENT);
 			}
 
 			@Test
@@ -275,12 +275,38 @@ class EingangStubReceiverStreamObserverTest {
 
 				assertThat(getGroupName()).isNull();
 			}
+
+			@Test
+			void shouldCallAddAsAttachment() {
+				when(attachmentCollector.collect(any())).thenReturn(Optional.of(incomingFile));
+
+				observer.handleAttachment(attachmentWithContent);
+
+				verify(observer).addAsAttachment(incomingFile);
+			}
+
+			@Test
+			void shouldNotCallAddAsAttachment() {
+				when(attachmentCollector.collect(any())).thenReturn(Optional.empty());
+
+				observer.handleAttachment(attachmentWithContent);
+
+				verify(observer, never()).addAsAttachment(any());
+			}
 		}
 	}
 
 	@Nested
 	class TestHandleRepresentation {
 
+		@Mock
+		private ContentCollector representationCollector;
+
+		@BeforeEach
+		void setUp() {
+			setRepresentationCollector(representationCollector);
+		}
+
 		@Nested
 		class TestWithFile {
 
@@ -289,7 +315,6 @@ class EingangStubReceiverStreamObserverTest {
 
 			@BeforeEach
 			void mock() {
-				doNothing().when(observer).setCurrentMetadata(any());
 				when(incomingFileMapper.fromGrpcRepresentationFile(any())).thenReturn(incomingFile);
 			}
 
@@ -301,17 +326,20 @@ class EingangStubReceiverStreamObserverTest {
 			}
 
 			@Test
-			void shouldCallSetCurrentMetadata() {
+			void shouldSetRepresentationCollector() {
+				var expectedContentCollector = ContentCollector.builder().fileSaver(fileSaver).incomingFile(incomingFile).build();
+
 				observer.handleRepresentation(representationWithFile);
 
-				verify(observer).setCurrentMetadata(incomingFile);
+				assertThat(getRepresentationCollector()).usingRecursiveComparison().ignoringFields("pipedInput", "pipedOutput")
+						.isEqualTo(expectedContentCollector);
 			}
 
 			@Test
-			void shouldNotCallHandleFileContent() {
+			void shouldNotCollectContent() {
 				observer.handleRepresentation(representationWithFile);
 
-				verify(observer, never()).handleFileContent(any());
+				verify(representationCollector, never()).collect(any());
 			}
 		}
 
@@ -319,17 +347,13 @@ class EingangStubReceiverStreamObserverTest {
 		class TestWithContent {
 
 			private final GrpcRepresentation representationWithContent = GrpcRepresentationTestFactory.createWithContent();
-
-			@BeforeEach
-			void mock() {
-				doNothing().when(observer).handleFileContent(any());
-			}
+			private final IncomingFile incomingFile = IncomingFileTestFactory.create();
 
 			@Test
-			void shouldCallHandleFileContent() {
+			void shouldCollectContent() {
 				observer.handleRepresentation(representationWithContent);
 
-				verify(observer).handleFileContent(GrpcRepresentationTestFactory.CONTENT);
+				verify(representationCollector).collect(GrpcRepresentationTestFactory.CONTENT);
 			}
 
 			@Test
@@ -338,549 +362,111 @@ class EingangStubReceiverStreamObserverTest {
 
 				verify(incomingFileMapper, never()).fromGrpcRepresentationFile(any());
 			}
-		}
-	}
-
-	@Nested
-	class TestSetCurrentMetadata {
-
-		private final IncomingFile incomingFile = IncomingFileTestFactory.create();
-
-		@Test
-		void shouldThrowIllegalStateExceptionIfCurrentFileIsSet() {
-			setCurrentFile(incomingFile);
-
-			assertThrows(IllegalStateException.class, () -> observer.setCurrentMetadata(incomingFile));
-		}
-
-		@Test
-		void shouldSetCurrentFile() {
-			observer.setCurrentMetadata(incomingFile);
-
-			assertThat(getCurrentFile()).isSameAs(incomingFile);
-		}
-	}
-
-	@Nested
-	class TestHandleFileContent {
-
-		private final GrpcFileContent fileContent = GrpcFileContentTestFactory.create();
-
-		@Nested
-		class TestOnReceivingFileContentIsNull {
-			@BeforeEach
-			void mock() {
-				doNothing().when(observer).initContentReceiving();
-				doNothing().when(observer).storeFileContent(any());
-			}
-
-			@Test
-			void shouldCallInitContentReceiving() {
-				observer.handleFileContent(fileContent);
-
-				verify(observer).initContentReceiving();
-			}
 
 			@Test
-			void shouldCallStoreFileContent() {
-				observer.handleFileContent(fileContent);
-
-				verify(observer).storeFileContent(fileContent);
-			}
-		}
-
-		@Nested
-		class TestOnReceivingFileContentIsNotNull {
-			@Mock
-			private CompletableFuture<File> receivingFileContent;
+			void shouldCallAddAsRepresentation() {
+				when(representationCollector.collect(any())).thenReturn(Optional.of(incomingFile));
 
-			@BeforeEach
-			void mock() {
-				doNothing().when(observer).storeFileContent(any());
-				setFileContent(receivingFileContent);
-			}
-
-			@Test
-			void shouldNotCallInitContentReceiving() {
-				observer.handleFileContent(fileContent);
-
-				verify(observer, never()).initContentReceiving();
-			}
-
-			@Test
-			void shouldCallStoreFileContent() {
-				observer.handleFileContent(fileContent);
+				observer.handleRepresentation(representationWithContent);
 
-				verify(observer).storeFileContent(fileContent);
+				verify(observer).addAsRepresentation(incomingFile);
 			}
-		}
-	}
-
-	@Nested
-	class TestInitContentReceiving {
-
-		private final byte[] content = new byte[] { 1, 2, 3 };
-
-		@Test
-		void shouldCreateInputStream() {
-			observer.initContentReceiving();
-
-			assertThat(getPipedInput()).isNotNull();
-		}
-
-		@Test
-		void shouldCreateOutputStream() {
-			observer.initContentReceiving();
-
-			assertThat(getPipedOutput()).isNotNull();
-		}
-
-		@Test
-		void shouldCreateConnectedStreams() {
-			observer.initContentReceiving();
-
-			verifyStreamSetUp();
-		}
-
-		@SneakyThrows
-		private void verifyStreamSetUp() {
-			var pipedInput = getPipedInput();
-			var pipedOutput = getPipedOutput();
-			pipedOutput.write(content);
-			pipedOutput.close();
-			var readBytes = pipedInput.readAllBytes();
-			assertThat(readBytes).isEqualTo(content);
-		}
-
-		@Test
-		void shouldCallFileSaver() {
-			observer.initContentReceiving();
-
-			verify(fileSaver).apply(getPipedInput());
-		}
-
-		@Test
-		void shouldSetReceivingFileContent() {
-			var fileFuture = CompletableFuture.completedFuture(mock(File.class));
-			when(fileSaver.apply(any())).thenReturn(fileFuture);
-
-			observer.initContentReceiving();
-
-			assertThat(getFileContent()).isSameAs(fileFuture);
-		}
-	}
-
-	@Nested
-	class TestStoreFileContent {
-		@Mock
-		private PipedOutputStream pipedOutput;
-
-		@BeforeEach
-		void setUp() {
-			setPipedOutput(pipedOutput);
-		}
-
-		@Nested
-		class TestOnCurrentFileIsNull {
 
 			@Test
-			void shouldThrowTechnicalException() {
-				var fileContent = GrpcFileContentTestFactory.create();
+			void shouldNotCallAddAsRepresentation() {
+				when(representationCollector.collect(any())).thenReturn(Optional.empty());
 
-				assertThrows(IllegalStateException.class, () -> observer.storeFileContent(fileContent));
-			}
-		}
-
-		@Nested
-		class TestOnCurrentFileIsNotNull {
-
-			private final IncomingFile incomingFile = IncomingFileTestFactory.create();
-
-			@BeforeEach
-			void mock() {
-				setCurrentFile(incomingFile);
-			}
-
-			@Nested
-			class TestOnEndOfFile {
-
-				private GrpcFileContent fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(true).build();
-
-				@BeforeEach
-				void setUp() {
-					doNothing().when(observer).handleEndOfFile();
-				}
-
-				@Test
-				void shouldCallHandleEndOfFile() {
-					observer.storeFileContent(fileContent);
-
-					verify(observer).handleEndOfFile();
-				}
-
-				@Test
-				@SneakyThrows
-				void shouldNotWriteContentToOutputStream() {
-					observer.storeFileContent(fileContent);
-
-					verify(pipedOutput, never()).write(any());
-				}
-			}
-
-			@Nested
-			class TestOnNotEndOfFile {
-
-				private GrpcFileContent fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(false).build();
-
-				@Test
-				@SneakyThrows
-				void shouldWriteContentToOutputStream() {
-					observer.storeFileContent(fileContent);
-
-					verify(pipedOutput).write(GrpcFileContentTestFactory.CONTENT);
-				}
-
-				@Test
-				void shouldNotCallHandleEndOfFile() {
-					observer.storeFileContent(fileContent);
-
-					verify(observer, never()).handleEndOfFile();
-				}
-
-				@Test
-				@SneakyThrows
-				void shouldThrowTechnicalExceptionOnIOException() {
-					doThrow(new IOException()).when(pipedOutput).write(any());
+				observer.handleRepresentation(representationWithContent);
 
-					assertThrows(TechnicalException.class, () -> {
-						observer.storeFileContent(fileContent);
-					});
-				}
+				verify(observer, never()).addAsRepresentation(any());
 			}
 		}
 	}
 
 	@Nested
-	class TestHandleEndOfFile {
+	class TestAddAsAttachment {
 
 		@Mock
-		private File savedFileContent;
+		private ContentCollector attachmentCollector;
 
-		private final IncomingFile incomingFile = IncomingFileTestFactory.createBuilder().file(null).build();
+		private final IncomingFile incomingFile = IncomingFileTestFactory.create();
 
 		@BeforeEach
 		void setUp() {
-			doNothing().when(observer).closeOutputPipe();
-			doReturn(savedFileContent).when(observer).getSavedFileContent();
-			setCurrentFile(incomingFile);
+			setRepresentationCollector(attachmentCollector);
+			setGroupName(GrpcAttachmentFileTestFactory.GROUP_NAME);
 		}
 
 		@Test
-		void shouldCallCloseOutputPipe() {
-			observer.handleEndOfFile();
-
-			verify(observer).closeOutputPipe();
-		}
-
-		@Nested
-		class TestOnGroupNameNull {
-
-			@BeforeEach
-			void setUp() {
-				setGroupName(null);
-			}
-
-			@Test
-			void shouldAddFileToRepresentations() {
-				var expectedIncomingFile = IncomingFileTestFactory.createBuilder().file(savedFileContent).build();
+		void shouldAddFileToAttachments() {
+			observer.addAsAttachment(incomingFile);
 
-				observer.handleEndOfFile();
-
-				assertThat(getRepresentations()).usingRecursiveFieldByFieldElementComparator().containsExactly(expectedIncomingFile);
-			}
-		}
-
-		@Nested
-		class TestOnGroupNameSet {
-
-			@BeforeEach
-			void setUp() {
-				setGroupName(GrpcAttachmentFileTestFactory.GROUP_NAME);
-			}
-
-			@Test
-			void shouldAddFileToAttachments() {
-				var expectedIncomingFile = IncomingFileTestFactory.createBuilder().file(savedFileContent).build();
-
-				observer.handleEndOfFile();
-
-				var attachmentGroup = getAttachments().get(GrpcAttachmentFileTestFactory.GROUP_NAME);
-				assertThat(attachmentGroup).usingRecursiveFieldByFieldElementComparator().containsExactly(expectedIncomingFile);
-			}
+			var attachmentGroup = getAttachments().get(GrpcAttachmentFileTestFactory.GROUP_NAME);
+			assertThat(attachmentGroup).usingRecursiveFieldByFieldElementComparator().containsExactly(incomingFile);
 		}
 
 		@Test
-		void shouldCallResetFileReceiving() {
-			observer.handleEndOfFile();
+		void shouldSetAttachmentCollectorToNull() {
+			observer.addAsAttachment(incomingFile);
 
-			verify(observer).resetFileReceiving();
+			assertThat(getAttachmentCollector()).isNull();
 		}
 	}
 
 	@Nested
-	class TestGetSavedFileContent {
-		@BeforeEach
-		void setUp() {
-			doNothing().when(observer).closeInputPipe();
-		}
+	class TestAddAsRepresentation {
 
-		@Nested
-		class TestOnNoExceptions {
-			@Mock
-			private File fileContent;
-
-			@BeforeEach
-			void setUp() {
-				setFileContent(CompletableFuture.completedFuture(fileContent));
-			}
-
-			@Test
-			void shouldReturnFile() {
-				var savedFileContent = observer.getSavedFileContent();
-
-				assertThat(savedFileContent).isSameAs(fileContent);
-			}
-
-			@Test
-			void shouldCallCloseInputPipe() {
-				observer.getSavedFileContent();
-
-				verify(observer).closeInputPipe();
-			}
-		}
-
-		@Nested
-		class TestOnExecutionException {
-
-			@BeforeEach
-			void setUp() {
-				setFileContent(CompletableFuture.failedFuture(new Exception()));
-			}
-
-			@Test
-			void shouldThrowTechnicalException() {
-				assertThrows(TechnicalException.class, () -> observer.getSavedFileContent());
-			}
-
-			@Test
-			void shouldCallCloseInputPipe() {
-				try {
-					observer.getSavedFileContent();
-				} catch (TechnicalException e) {
-					// expected
-				}
-
-				verify(observer).closeInputPipe();
-			}
-		}
-
-		@Nested
-		class TestOnTimeoutException {
-
-			@Mock
-			private CompletableFuture<File> fileFuture;
-
-			@BeforeEach
-			@SneakyThrows
-			void setUp() {
-				setFileContent(fileFuture);
-				when(fileFuture.get(anyLong(), any())).thenThrow(new TimeoutException());
-			}
-
-			@Test
-			void shouldThrowTechnicalException() {
-				assertThrows(TechnicalException.class, () -> observer.getSavedFileContent());
-			}
-
-			@Test
-			void shouldCallCloseInputPipe() {
-				try {
-					observer.getSavedFileContent();
-				} catch (TechnicalException e) {
-					// expected
-				}
-
-				verify(observer).closeInputPipe();
-			}
-		}
-
-		@Nested
-		class TestOnInterruptedException {
-
-			@Mock
-			private CompletableFuture<File> fileFuture;
-
-			@BeforeEach
-			@SneakyThrows
-			void setUp() {
-				setFileContent(fileFuture);
-				when(fileFuture.get(anyLong(), any())).thenThrow(new InterruptedException());
-			}
-
-			@Test
-			void shouldThrowTechnicalException() {
-				assertThrows(TechnicalException.class, () -> observer.getSavedFileContent());
-			}
-
-			@Test
-			void shouldInterruptCurrentThread() {
-				try {
-					observer.getSavedFileContent();
-				} catch (TechnicalException e) {
-					// expected
-				}
-
-				assertThat(Thread.currentThread().isInterrupted()).isTrue();
-			}
-
-			@Test
-			void shouldCallCloseInputPipe() {
-				try {
-					observer.getSavedFileContent();
-				} catch (TechnicalException e) {
-					// expected
-				}
-
-				verify(observer).closeInputPipe();
-			}
-		}
-	}
+		@Mock
+		private ContentCollector representationCollector;
 
-	@Nested
-	class TestResetFielReceiving {
+		private final IncomingFile incomingFile = IncomingFileTestFactory.create();
 
 		@BeforeEach
 		void setUp() {
-			setCurrentFile(IncomingFileTestFactory.create());
-			setGroupName(GrpcAttachmentFileTestFactory.GROUP_NAME);
-			setPipedOutput(mock(PipedOutputStream.class));
-			setPipedOutput(mock(PipedOutputStream.class));
-			setFileContent(CompletableFuture.completedFuture(mock(File.class)));
-		}
-
-		@Test
-		void shouldResetCurrentFile() {
-			observer.resetFileReceiving();
-
-			assertThat(getCurrentFile()).isNull();
-		}
-
-		@Test
-		void shouldResetGroupName() {
-			observer.resetFileReceiving();
-
-			assertThat(getGroupName()).isNull();
-		}
-
-		@Test
-		void shouldResetPipedOutput() {
-			observer.resetFileReceiving();
-
-			assertThat(getPipedOutput()).isNull();
+			setRepresentationCollector(representationCollector);
 		}
 
 		@Test
-		void shouldResetPipedInput() {
-			observer.resetFileReceiving();
+		void shouldAddFileToRepresentations() {
+			observer.addAsRepresentation(incomingFile);
 
-			assertThat(getPipedInput()).isNull();
+			assertThat(getRepresentations()).usingRecursiveFieldByFieldElementComparator().containsExactly(incomingFile);
 		}
 
 		@Test
-		void shouldResetReceivingFileContent() {
-			observer.resetFileReceiving();
+		void shouldSetRepresentationCollectorToNull() {
+			observer.addAsRepresentation(incomingFile);
 
-			assertThat(getFileContent()).isNull();
+			assertThat(getRepresentationCollector()).isNull();
 		}
 	}
 
 	@Nested
 	class TestOnError {
 
-		@BeforeEach
-		void mock() {
-			doNothing().when(observer).closeOutputPipe();
-			doNothing().when(observer).closeInputPipe();
-		}
-
-		@Test
-		void shouldCallCloseOutputPipe() {
-			observer.onError(new Exception());
-
-			verify(observer).closeOutputPipe();
-		}
-
-		@Test
-		void shouldCallCloseInputPipe() {
-			observer.onError(new Exception());
-
-			verify(observer).closeInputPipe();
-		}
-	}
-
-	@Nested
-	class TestCloseOutputPipe {
-
 		@Mock
-		private PipedOutputStream pipedOutput;
-
-		@BeforeEach
-		void setUp() {
-			setPipedOutput(pipedOutput);
-		}
-
-		@Test
-		@SneakyThrows
-		void shouldClosePipedOutput() {
-			observer.closeOutputPipe();
-
-			verify(pipedOutput).close();
-		}
-
-		@Test
-		@SneakyThrows
-		void shouldNotThrowException() {
-			doThrow(IOException.class).when(pipedOutput).close();
-
-			assertDoesNotThrow(() -> observer.closeOutputPipe());
-		}
-	}
-
-	@Nested
-	class TestCloseInputPipe {
-
+		private ContentCollector attachmentCollector;
 		@Mock
-		private PipedInputStream pipedInput;
+		private ContentCollector representationCollector;
 
 		@BeforeEach
-		void setUp() {
-			setPipedInput(pipedInput);
+		void mock() {
+			setAttachmentCollector(attachmentCollector);
+			setRepresentationCollector(representationCollector);
 		}
 
 		@Test
-		@SneakyThrows
-		void shouldClosePipedInput() {
-			observer.closeInputPipe();
+		void shouldCloseAttachmentCollector() {
+			observer.onError(new Exception());
 
-			verify(pipedInput).close();
+			verify(attachmentCollector).close();
 		}
 
 		@Test
-		@SneakyThrows
-		void shouldNotThrowException() {
-			doThrow(IOException.class).when(pipedInput).close();
+		void shouldCloseRepresentationCollector() {
+			observer.onError(new Exception());
 
-			assertDoesNotThrow(() -> observer.closeInputPipe());
+			verify(representationCollector).close();
 		}
 	}
 
@@ -987,39 +573,6 @@ class EingangStubReceiverStreamObserverTest {
 		ReflectionTestUtils.setField(observer, "groupName", groupName);
 	}
 
-	private IncomingFile getCurrentFile() {
-		return (IncomingFile) ReflectionTestUtils.getField(observer, "currentFile");
-	}
-
-	private void setCurrentFile(IncomingFile incomingFile) {
-		ReflectionTestUtils.setField(observer, "currentFile", incomingFile);
-	}
-
-	private void setFileContent(CompletableFuture<File> fileFuture) {
-		ReflectionTestUtils.setField(observer, "receivingFileContent", fileFuture);
-	}
-
-	@SuppressWarnings("unchecked")
-	private CompletableFuture<File> getFileContent() {
-		return (CompletableFuture<File>) ReflectionTestUtils.getField(observer, "receivingFileContent");
-	}
-
-	private PipedInputStream getPipedInput() {
-		return (PipedInputStream) ReflectionTestUtils.getField(observer, "pipedInput");
-	}
-
-	private void setPipedInput(PipedInputStream pipedInput) {
-		ReflectionTestUtils.setField(observer, "pipedInput", pipedInput);
-	}
-
-	private PipedOutputStream getPipedOutput() {
-		return (PipedOutputStream) ReflectionTestUtils.getField(observer, "pipedOutput");
-	}
-
-	private void setPipedOutput(PipedOutputStream pipedOutput) {
-		ReflectionTestUtils.setField(observer, "pipedOutput", pipedOutput);
-	}
-
 	@SuppressWarnings("unchecked")
 	private List<IncomingFile> getRepresentations() {
 		return (List<IncomingFile>) ReflectionTestUtils.getField(observer, "representations");
@@ -1038,4 +591,20 @@ class EingangStubReceiverStreamObserverTest {
 		ReflectionTestUtils.setField(observer, "attachments", attachments);
 	}
 
+	private ContentCollector getAttachmentCollector() {
+		return (ContentCollector) ReflectionTestUtils.getField(observer, "attachmentCollector");
+	}
+
+	private void setAttachmentCollector(ContentCollector attachmentCollector) {
+		ReflectionTestUtils.setField(observer, "attachmentCollector", attachmentCollector);
+	}
+
+	private ContentCollector getRepresentationCollector() {
+		return (ContentCollector) ReflectionTestUtils.getField(observer, "representationCollector");
+	}
+
+	private void setRepresentationCollector(ContentCollector representationCollector) {
+		ReflectionTestUtils.setField(observer, "representationCollector", representationCollector);
+	}
+
 }
-- 
GitLab


From 9abbd56222cb1db5a0a40e8a968fe6a59d16e567 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Fri, 21 Mar 2025 09:17:59 +0100
Subject: [PATCH 26/28] OZG-7573 refactor EingangStubReceiverStreamObserver

---
 .../EingangStubReceiverStreamObserver.java    |  40 +++--
 ...EingangStubReceiverStreamObserverTest.java | 164 +++++++++++-------
 2 files changed, 129 insertions(+), 75 deletions(-)

diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
index 45f253538..40dbc1b1a 100644
--- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
+++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java
@@ -72,8 +72,7 @@ class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwa
 	private final Map<String, List<IncomingFile>> attachments = new HashMap<>();
 
 	private String groupName;
-	private ContentCollector attachmentCollector;
-	private ContentCollector representationCollector;
+	private ContentCollector contentCollector;
 
 	@Override
 	public synchronized void onNext(GrpcRouteForwardingRequest request) {
@@ -97,24 +96,28 @@ class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwa
 
 	void handleAttachment(GrpcAttachment attachment) {
 		if (attachment.hasFile()) {
-			attachmentCollector = buildContentCollector(incomingFileMapper.fromGrpcAttachmentFile(attachment.getFile()));
+			verifyNoOngoingFileDownload();
+			contentCollector = buildContentCollector(incomingFileMapper.fromGrpcAttachmentFile(attachment.getFile()));
 			groupName = attachment.getFile().getGroupName();
 		} else {
-			if (Objects.isNull(attachmentCollector)) {
-				throw new IllegalStateException("File content received before metadata.");
-			}
-			attachmentCollector.collect(attachment.getContent()).ifPresent(this::addAsAttachment);
+			verifyMetadataWasSent();
+			contentCollector.collect(attachment.getContent()).ifPresent(this::addAsAttachment);
 		}
 	}
 
 	void handleRepresentation(GrpcRepresentation representation) {
 		if (representation.hasFile()) {
-			representationCollector = buildContentCollector(incomingFileMapper.fromGrpcRepresentationFile(representation.getFile()));
+			verifyNoOngoingFileDownload();
+			contentCollector = buildContentCollector(incomingFileMapper.fromGrpcRepresentationFile(representation.getFile()));
 		} else {
-			if (Objects.isNull(representationCollector)) {
-				throw new IllegalStateException("File content received before metadata.");
-			}
-			representationCollector.collect(representation.getContent()).ifPresent(this::addAsRepresentation);
+			verifyMetadataWasSent();
+			contentCollector.collect(representation.getContent()).ifPresent(this::addAsRepresentation);
+		}
+	}
+
+	void verifyNoOngoingFileDownload() {
+		if (Objects.nonNull(contentCollector)) {
+			throw new IllegalStateException("File metadata received before previous file content was completed.");
 		}
 	}
 
@@ -125,21 +128,26 @@ class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRouteForwa
 				.build();
 	}
 
+	void verifyMetadataWasSent() {
+		if (Objects.isNull(contentCollector)) {
+			throw new IllegalStateException("File content received before metadata.");
+		}
+	}
+
 	void addAsAttachment(IncomingFile completedIncomingFile) {
 		attachments.computeIfAbsent(groupName, s -> new ArrayList<>()).add(completedIncomingFile);
-		attachmentCollector = null;
+		contentCollector = null;
 	}
 
 	void addAsRepresentation(IncomingFile completedIncomingFile) {
 		representations.add(completedIncomingFile);
-		representationCollector = null;
+		contentCollector = null;
 	}
 
 	@Override
 	public synchronized void onError(Throwable t) {
 		LOG.error("Error happened. Receiving stream closed.", t);
-		attachmentCollector.close();
-		representationCollector.close();
+		contentCollector.close();
 	}
 
 	@Override
diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
index f312ec86f..c8e6dce6b 100644
--- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
+++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java
@@ -199,12 +199,7 @@ class EingangStubReceiverStreamObserverTest {
 	class TestHandleAttachment {
 
 		@Mock
-		private ContentCollector attachmentCollector;
-
-		@BeforeEach
-		void setUp() {
-			setAttachmentCollector(attachmentCollector);
-		}
+		private ContentCollector contentCollector;
 
 		@Nested
 		class TestWithFile {
@@ -213,8 +208,16 @@ class EingangStubReceiverStreamObserverTest {
 			private final IncomingFile incomingFile = IncomingFileTestFactory.create();
 
 			@BeforeEach
-			void mock() {
+			void initMock() {
 				when(incomingFileMapper.fromGrpcAttachmentFile(any())).thenReturn(incomingFile);
+				doNothing().when(observer).verifyNoOngoingFileDownload();
+			}
+
+			@Test
+			void shouldCallVerifyNoOngoingFileDownload() {
+				observer.handleAttachment(attachmentWithFile);
+
+				verify(observer).verifyNoOngoingFileDownload();
 			}
 
 			@Test
@@ -225,12 +228,12 @@ class EingangStubReceiverStreamObserverTest {
 			}
 
 			@Test
-			void shouldSetAttachmentCollector() {
+			void shouldSetContentCollector() {
 				var expectedContentCollector = ContentCollector.builder().fileSaver(fileSaver).incomingFile(incomingFile).build();
 
 				observer.handleAttachment(attachmentWithFile);
 
-				assertThat(getAttachmentCollector()).usingRecursiveComparison().ignoringFields("pipedInput", "pipedOutput")
+				assertThat(getContentCollector()).usingRecursiveComparison().ignoringFields("pipedInput", "pipedOutput")
 						.isEqualTo(expectedContentCollector);
 			}
 
@@ -245,7 +248,7 @@ class EingangStubReceiverStreamObserverTest {
 			void shouldNotCollectContent() {
 				observer.handleAttachment(attachmentWithFile);
 
-				verify(attachmentCollector, never()).collect(any());
+				verify(contentCollector, never()).collect(any());
 			}
 		}
 
@@ -255,11 +258,23 @@ class EingangStubReceiverStreamObserverTest {
 			private final GrpcAttachment attachmentWithContent = GrpcAttachmentTestFactory.createWithContent();
 			private final IncomingFile incomingFile = IncomingFileTestFactory.create();
 
+			@BeforeEach
+			void setUp() {
+				setContentCollector(contentCollector);
+			}
+
+			@Test
+			void shouldCallVerifyMetadataWasSent() {
+				observer.handleAttachment(attachmentWithContent);
+
+				verify(observer).verifyMetadataWasSent();
+			}
+
 			@Test
 			void shouldCollectContent() {
 				observer.handleAttachment(attachmentWithContent);
 
-				verify(attachmentCollector).collect(GrpcAttachmentTestFactory.CONTENT);
+				verify(contentCollector).collect(GrpcAttachmentTestFactory.CONTENT);
 			}
 
 			@Test
@@ -278,7 +293,7 @@ class EingangStubReceiverStreamObserverTest {
 
 			@Test
 			void shouldCallAddAsAttachment() {
-				when(attachmentCollector.collect(any())).thenReturn(Optional.of(incomingFile));
+				when(contentCollector.collect(any())).thenReturn(Optional.of(incomingFile));
 
 				observer.handleAttachment(attachmentWithContent);
 
@@ -287,12 +302,13 @@ class EingangStubReceiverStreamObserverTest {
 
 			@Test
 			void shouldNotCallAddAsAttachment() {
-				when(attachmentCollector.collect(any())).thenReturn(Optional.empty());
+				when(contentCollector.collect(any())).thenReturn(Optional.empty());
 
 				observer.handleAttachment(attachmentWithContent);
 
 				verify(observer, never()).addAsAttachment(any());
 			}
+
 		}
 	}
 
@@ -300,12 +316,7 @@ class EingangStubReceiverStreamObserverTest {
 	class TestHandleRepresentation {
 
 		@Mock
-		private ContentCollector representationCollector;
-
-		@BeforeEach
-		void setUp() {
-			setRepresentationCollector(representationCollector);
-		}
+		private ContentCollector contentCollector;
 
 		@Nested
 		class TestWithFile {
@@ -316,6 +327,14 @@ class EingangStubReceiverStreamObserverTest {
 			@BeforeEach
 			void mock() {
 				when(incomingFileMapper.fromGrpcRepresentationFile(any())).thenReturn(incomingFile);
+				doNothing().when(observer).verifyNoOngoingFileDownload();
+			}
+
+			@Test
+			void shouldCallVerifyNoOngoingFileDownload() {
+				observer.handleRepresentation(representationWithFile);
+
+				verify(observer).verifyNoOngoingFileDownload();
 			}
 
 			@Test
@@ -326,12 +345,12 @@ class EingangStubReceiverStreamObserverTest {
 			}
 
 			@Test
-			void shouldSetRepresentationCollector() {
+			void shouldSetContentCollector() {
 				var expectedContentCollector = ContentCollector.builder().fileSaver(fileSaver).incomingFile(incomingFile).build();
 
 				observer.handleRepresentation(representationWithFile);
 
-				assertThat(getRepresentationCollector()).usingRecursiveComparison().ignoringFields("pipedInput", "pipedOutput")
+				assertThat(getContentCollector()).usingRecursiveComparison().ignoringFields("pipedInput", "pipedOutput")
 						.isEqualTo(expectedContentCollector);
 			}
 
@@ -339,7 +358,7 @@ class EingangStubReceiverStreamObserverTest {
 			void shouldNotCollectContent() {
 				observer.handleRepresentation(representationWithFile);
 
-				verify(representationCollector, never()).collect(any());
+				verify(contentCollector, never()).collect(any());
 			}
 		}
 
@@ -349,11 +368,23 @@ class EingangStubReceiverStreamObserverTest {
 			private final GrpcRepresentation representationWithContent = GrpcRepresentationTestFactory.createWithContent();
 			private final IncomingFile incomingFile = IncomingFileTestFactory.create();
 
+			@BeforeEach
+			void setUp() {
+				setContentCollector(contentCollector);
+			}
+
+			@Test
+			void shouldCallVerifyMetadataWasSent() {
+				observer.handleRepresentation(representationWithContent);
+
+				verify(observer).verifyMetadataWasSent();
+			}
+
 			@Test
 			void shouldCollectContent() {
 				observer.handleRepresentation(representationWithContent);
 
-				verify(representationCollector).collect(GrpcRepresentationTestFactory.CONTENT);
+				verify(contentCollector).collect(GrpcRepresentationTestFactory.CONTENT);
 			}
 
 			@Test
@@ -365,7 +396,7 @@ class EingangStubReceiverStreamObserverTest {
 
 			@Test
 			void shouldCallAddAsRepresentation() {
-				when(representationCollector.collect(any())).thenReturn(Optional.of(incomingFile));
+				when(contentCollector.collect(any())).thenReturn(Optional.of(incomingFile));
 
 				observer.handleRepresentation(representationWithContent);
 
@@ -374,7 +405,7 @@ class EingangStubReceiverStreamObserverTest {
 
 			@Test
 			void shouldNotCallAddAsRepresentation() {
-				when(representationCollector.collect(any())).thenReturn(Optional.empty());
+				when(contentCollector.collect(any())).thenReturn(Optional.empty());
 
 				observer.handleRepresentation(representationWithContent);
 
@@ -383,17 +414,49 @@ class EingangStubReceiverStreamObserverTest {
 		}
 	}
 
+	@Nested
+	class TestVerifyNoOngoingFileDownload {
+
+		@Test
+		void shouldThrowIllegalStateExceptionIfContentCollectorIsNotNull() {
+			setContentCollector(mock(ContentCollector.class));
+
+			assertThrows(IllegalStateException.class, () -> observer.verifyNoOngoingFileDownload());
+		}
+
+		@Test
+		void shouldNotThrowExceptionIfContentCollectorIsNull() {
+			assertDoesNotThrow(() -> observer.verifyNoOngoingFileDownload());
+		}
+	}
+
+	@Nested
+	class TestVerifyMetadataWasSent {
+
+		@Test
+		void shouldThrowIllegalStateExceptionIfContentCollectorIsNull() {
+			assertThrows(IllegalStateException.class, () -> observer.verifyMetadataWasSent());
+		}
+
+		@Test
+		void shouldNotThrowExceptionIfContentCollectorIsNotNull() {
+			setContentCollector(mock(ContentCollector.class));
+
+			assertDoesNotThrow(() -> observer.verifyMetadataWasSent());
+		}
+	}
+
 	@Nested
 	class TestAddAsAttachment {
 
 		@Mock
-		private ContentCollector attachmentCollector;
+		private ContentCollector contentCollector;
 
 		private final IncomingFile incomingFile = IncomingFileTestFactory.create();
 
 		@BeforeEach
 		void setUp() {
-			setRepresentationCollector(attachmentCollector);
+			setContentCollector(contentCollector);
 			setGroupName(GrpcAttachmentFileTestFactory.GROUP_NAME);
 		}
 
@@ -406,10 +469,10 @@ class EingangStubReceiverStreamObserverTest {
 		}
 
 		@Test
-		void shouldSetAttachmentCollectorToNull() {
+		void shouldSetContentCollectorToNull() {
 			observer.addAsAttachment(incomingFile);
 
-			assertThat(getAttachmentCollector()).isNull();
+			assertThat(getContentCollector()).isNull();
 		}
 	}
 
@@ -417,13 +480,13 @@ class EingangStubReceiverStreamObserverTest {
 	class TestAddAsRepresentation {
 
 		@Mock
-		private ContentCollector representationCollector;
+		private ContentCollector contentCollector;
 
 		private final IncomingFile incomingFile = IncomingFileTestFactory.create();
 
 		@BeforeEach
 		void setUp() {
-			setRepresentationCollector(representationCollector);
+			setContentCollector(contentCollector);
 		}
 
 		@Test
@@ -434,10 +497,10 @@ class EingangStubReceiverStreamObserverTest {
 		}
 
 		@Test
-		void shouldSetRepresentationCollectorToNull() {
+		void shouldSetContentCollectorToNull() {
 			observer.addAsRepresentation(incomingFile);
 
-			assertThat(getRepresentationCollector()).isNull();
+			assertThat(getContentCollector()).isNull();
 		}
 	}
 
@@ -445,28 +508,19 @@ class EingangStubReceiverStreamObserverTest {
 	class TestOnError {
 
 		@Mock
-		private ContentCollector attachmentCollector;
-		@Mock
-		private ContentCollector representationCollector;
+		private ContentCollector contentCollector;
 
 		@BeforeEach
 		void mock() {
-			setAttachmentCollector(attachmentCollector);
-			setRepresentationCollector(representationCollector);
+			setContentCollector(contentCollector);
+			setContentCollector(contentCollector);
 		}
 
 		@Test
-		void shouldCloseAttachmentCollector() {
+		void shouldCloseContentCollector() {
 			observer.onError(new Exception());
 
-			verify(attachmentCollector).close();
-		}
-
-		@Test
-		void shouldCloseRepresentationCollector() {
-			observer.onError(new Exception());
-
-			verify(representationCollector).close();
+			verify(contentCollector).close();
 		}
 	}
 
@@ -591,20 +645,12 @@ class EingangStubReceiverStreamObserverTest {
 		ReflectionTestUtils.setField(observer, "attachments", attachments);
 	}
 
-	private ContentCollector getAttachmentCollector() {
-		return (ContentCollector) ReflectionTestUtils.getField(observer, "attachmentCollector");
-	}
-
-	private void setAttachmentCollector(ContentCollector attachmentCollector) {
-		ReflectionTestUtils.setField(observer, "attachmentCollector", attachmentCollector);
-	}
-
-	private ContentCollector getRepresentationCollector() {
-		return (ContentCollector) ReflectionTestUtils.getField(observer, "representationCollector");
+	private ContentCollector getContentCollector() {
+		return (ContentCollector) ReflectionTestUtils.getField(observer, "contentCollector");
 	}
 
-	private void setRepresentationCollector(ContentCollector representationCollector) {
-		ReflectionTestUtils.setField(observer, "representationCollector", representationCollector);
+	private void setContentCollector(ContentCollector contentCollector) {
+		ReflectionTestUtils.setField(observer, "contentCollector", contentCollector);
 	}
 
 }
-- 
GitLab


From ea283fb0f27254f85d35624d273ce7dfbce7c7c8 Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Fri, 21 Mar 2025 12:32:05 +0100
Subject: [PATCH 27/28] OZG-7573 add ingress for vorgang-manager

---
 src/main/helm/templates/network_policy.yaml | 22 ++++++++----
 src/test/helm/network_policy_test.yaml      | 39 +++++++++++++++++++++
 2 files changed, 55 insertions(+), 6 deletions(-)

diff --git a/src/main/helm/templates/network_policy.yaml b/src/main/helm/templates/network_policy.yaml
index 17f7bc782..b4bd33ed1 100644
--- a/src/main/helm/templates/network_policy.yaml
+++ b/src/main/helm/templates/network_policy.yaml
@@ -22,12 +22,12 @@
 # unter der Lizenz sind dem Lizenztext zu entnehmen.
 #
 
-{{- if not (.Values.networkPolicy).disabled }} 
+{{- if not (.Values.networkPolicy).disabled }}
 apiVersion: networking.k8s.io/v1
 kind: NetworkPolicy
 metadata:
   name: network-policy-{{ .Release.Name}}
-  namespace: {{ .Release.Namespace }} 
+  namespace: {{ .Release.Namespace }}
 spec:
   podSelector:
     matchLabels:
@@ -45,9 +45,19 @@ spec:
     ports:
     - protocol: TCP
       port: 8081
+  {{- if (.Values.forwarding).enabled }}
+  - ports:
+    - port: 9090
+      protocol: TCP
+    from:
+    - namespaceSelector: {}
+      podSelector:
+        matchLabels:
+          component: vorgang-manager
+  {{- end }}
   egress:
   - to:
-    - podSelector: 
+    - podSelector:
         matchLabels:
           component: vorgang-manager
     ports:
@@ -72,7 +82,7 @@ spec:
     - namespaceSelector:
         matchLabels:
           kubernetes.io/metadata.name: {{ required "routing.zufiManager.namespace must be set if routingStrategy=ZUFI" ((.Values.routing).zufiManager).namespace }}
-      podSelector: 
+      podSelector:
         matchLabels:
           component: zufi-server
     ports:
@@ -80,7 +90,7 @@ spec:
         protocol: TCP
   - to:
     - namespaceSelector: {}
-      podSelector: 
+      podSelector:
         matchLabels:
           component: vorgang-manager
     ports:
@@ -88,4 +98,4 @@ spec:
         protocol: TCP
 {{- end }}
 
-{{- end }} 
\ No newline at end of file
+{{- end }}
\ No newline at end of file
diff --git a/src/test/helm/network_policy_test.yaml b/src/test/helm/network_policy_test.yaml
index d10537996..be4c627e5 100644
--- a/src/test/helm/network_policy_test.yaml
+++ b/src/test/helm/network_policy_test.yaml
@@ -222,3 +222,42 @@ tests:
             ports:
               - port: 9090
                 protocol: TCP
+
+  - it: should add ingress from vorgang-manager if forwarding is enabled
+    set:
+      networkPolicy:
+        dnsServerNamespace: test-dns-server-namespace
+      forwarding:
+        enabled: true
+    asserts:
+      - contains:
+          path: spec.ingress
+          content:
+            from:
+              - namespaceSelector: {}
+                podSelector:
+                  matchLabels:
+                    component: vorgang-manager
+            ports:
+              - port: 9090
+                protocol: TCP
+
+  - it: should not add ingress from vorgang-manager if forwarding is disabled
+    set:
+      networkPolicy:
+        dnsServerNamespace: test-dns-server-namespace
+      forwarding:
+        enabled: false
+    asserts:
+      - notContains:
+          path: spec.ingress
+          content:
+            from:
+              - namespaceSelector: {}
+                podSelector:
+                  matchLabels:
+                    component: vorgang-manager
+            ports:
+              - port: 9090
+                protocol: TCP
+          any: true
-- 
GitLab


From 0be6ccf29fb527407ed84948a64dda762c44cf7c Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Fri, 21 Mar 2025 12:36:35 +0100
Subject: [PATCH 28/28] OZG-7573 add forwarder to fs-adapter

---
 formcycle-adapter/pom.xml | 4 ++++
 pom.xml                   | 5 +++++
 2 files changed, 9 insertions(+)

diff --git a/formcycle-adapter/pom.xml b/formcycle-adapter/pom.xml
index 2e7f85ed2..04849487d 100644
--- a/formcycle-adapter/pom.xml
+++ b/formcycle-adapter/pom.xml
@@ -63,6 +63,10 @@
 			<groupId>de.ozgcloud.vorgang</groupId>
 			<artifactId>vorgang-manager-interface</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>de.ozgcloud.eingang</groupId>
+			<artifactId>forwarder</artifactId>
+		</dependency>
 		<dependency>
 			<groupId>de.ozgcloud.vorgang</groupId>
 			<artifactId>vorgang-manager-interface</artifactId>
diff --git a/pom.xml b/pom.xml
index 82d197342..4d92bb83e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,6 +86,11 @@
 				<artifactId>router</artifactId>
 				<version>${project.version}</version>
 			</dependency>
+			<dependency>
+				<groupId>de.ozgcloud.eingang</groupId>
+				<artifactId>forwarder</artifactId>
+				<version>${project.version}</version>
+			</dependency>
 			<dependency>
 				<groupId>de.ozgcloud.eingang</groupId>
 				<artifactId>semantik-adapter</artifactId>
-- 
GitLab