From 63af33d00392750832c5645275277b925e47b0af Mon Sep 17 00:00:00 2001
From: Michael Hoennig <michael.hoennig@hostsharing.net>
Date: Tue, 5 Nov 2024 13:58:31 +0100
Subject: [PATCH] feature/use-case-acceptance-tests-2 (#117)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/117
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
---
 Jenkinsfile                                   |  36 ++++-
 build.gradle                                  |  55 ++++++-
 doc/scenarios/template.html                   | 124 ++++++++++++++++
 etc/jenkinsAgent.Dockerfile                   |   6 +-
 .../hs-office/hs-office-sepamandates.yaml     |   2 +-
 .../scenarios/HsOfficeScenarioTests.java      | 112 ++++++++++++--
 .../hs/office/scenarios/PathAssertion.java    |  23 +++
 .../hsadminng/hs/office/scenarios/README.md   |  32 +++-
 .../hs/office/scenarios/ScenarioTest.java     |   4 +-
 .../hs/office/scenarios/TemplateResolver.java | 140 +++++++++++++-----
 .../scenarios/TemplateResolverUnitTest.java   |  73 +++++++++
 .../hs/office/scenarios/TestReport.java       |   4 +-
 .../hs/office/scenarios/UseCase.java          |  78 ++++++++--
 .../scenarios/UseCaseNotImplementedYet.java   |  16 ++
 .../contact/AddPhoneNumberToContactData.java  |  51 +++++++
 .../scenarios/contact/AmendContactData.java   |  44 ++++++
 .../RemovePhoneNumberFromContactData.java     |  50 +++++++
 .../scenarios/contact/ReplaceContactData.java |  64 ++++++++
 .../CreateExternalDebitorForPartner.java      |   2 +-
 .../debitor/CreateSelfDebitorForPartner.java  |   3 +-
 .../debitor/CreateSepaMandateForDebitor.java  |  20 ++-
 .../scenarios/debitor/DeleteDebitor.java      |   6 +-
 .../debitor/DeleteSepaMandateForDebitor.java  |  20 ---
 .../debitor/DontDeleteDefaultDebitor.java     |   2 +-
 .../FinallyDeleteSepaMandateForDebitor.java   |  31 ++++
 .../InvalidateSepaMandateForDebitor.java      |  15 +-
 .../AddOperationsContactToPartner.java        |  13 +-
 .../partner/AddRepresentativeToPartner.java   |  12 +-
 .../scenarios/partner/CreatePartner.java      |  29 +++-
 .../scenarios/partner/DeletePartner.java      |   6 +-
 .../office/scenarios/person/CreatePerson.java |   6 +-
 .../RemoveOperationsContactFromPartner.java   |  26 +++-
 .../subscription/SubscribeToMailinglist.java  |   2 +-
 .../UnsubscribeFromMailinglist.java           |   8 +-
 ...ceSepaMandateControllerAcceptanceTest.java |  33 ++++-
 .../reflection/AnnotationFinder.java          |   0
 36 files changed, 1008 insertions(+), 140 deletions(-)
 create mode 100644 doc/scenarios/template.html
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java
 delete mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteSepaMandateForDebitor.java
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java
 rename src/{main => test}/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java (100%)

diff --git a/Jenkinsfile b/Jenkinsfile
index 5c1722d6..dc466d28 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -26,9 +26,35 @@ pipeline {
             }
         }
 
-        stage ('Compile & Test') {
+        stage ('Compile') {
             steps {
-                sh './gradlew clean check --no-daemon -x pitest -x dependencyCheckAnalyze'
+                sh './gradlew clean processSpring compileJava compileTestJava --no-daemon'
+            }
+        }
+
+        stage ('Tests') {
+            parallel {
+                stage('Unit-/Integration/Acceptance-Tests') {
+                    steps {
+                        sh './gradlew check --no-daemon -x pitest -x dependencyCheckAnalyze -x importOfficeData -x importHostingAssets'
+                    }
+                }
+                stage('Import-Tests') {
+                    steps {
+                        sh './gradlew importOfficeData importHostingAssets --no-daemon'
+                    }
+                }
+                stage ('Scenario-Tests') {
+                    steps {
+                        sh './gradlew scenarioTests --no-daemon'
+                    }
+                }
+            }
+        }
+
+        stage ('Check') {
+            steps {
+                sh './gradlew check -x pitest -x dependencyCheckAnalyze --no-daemon'
             }
         }
     }
@@ -45,6 +71,12 @@ pipeline {
                 sourcePattern: 'src/main/java'
             )
 
+            // archive scenario-test reports in HTML format
+            sh '''
+                ./gradlew convertMarkdownToHtml
+            '''
+            archiveArtifacts artifacts: 'doc/scenarios/*.html', allowEmptyArchive: true
+
             // cleanup workspace
             cleanWs()
         }
diff --git a/build.gradle b/build.gradle
index 96b16673..ed7a290d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -255,7 +255,7 @@ test {
             'net.hostsharing.hsadminng.**.generated.**',
     ]
     useJUnitPlatform {
-        excludeTags 'import'
+        excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest'
     }
 }
 jacocoTestReport {
@@ -344,6 +344,17 @@ tasks.register('importHostingAssets', Test) {
     mustRunAfter spotlessJava
 }
 
+tasks.register('scenarioTests', Test) {
+    useJUnitPlatform {
+        includeTags 'scenarioTest'
+    }
+
+    group 'verification'
+    description 'run the import jobs as tests'
+
+    mustRunAfter spotlessJava
+}
+
 // pitest mutation testing
 pitest {
     targetClasses = ['net.hostsharing.hsadminng.**']
@@ -391,3 +402,45 @@ tasks.named("dependencyUpdates").configure {
         isNonStable(it.candidate.version)
     }
 }
+
+
+// Generate HTML from Markdown scenario-test-reports using Pandoc:
+tasks.register('convertMarkdownToHtml') {
+    description = 'Generates HTML from Markdown scenario-test-reports using Pandoc.'
+    group = 'Conversion'
+
+    // Define the template file and input directory
+    def templateFile = file('doc/scenarios/template.html')
+
+    // Task configuration and execution
+    doFirst {
+        // Check if pandoc is installed
+        try {
+            exec {
+                commandLine 'pandoc', '--version'
+            }
+        } catch (Exception) {
+            throw new GradleException("Pandoc is not installed or not found in the system path.")
+        }
+
+        // Check if the template file exists
+        if (!templateFile.exists()) {
+            throw new GradleException("Template file 'doc/scenarios/template.html' not found.")
+        }
+    }
+
+    doLast {
+        // Gather all Markdown files in the current directory
+        fileTree(dir: '.', include: 'doc/scenarios/*.md').each { file ->
+            // Corrected way to create the output file path
+            def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html'))
+
+            // Execute pandoc for each markdown file
+            exec {
+                commandLine 'pandoc', file.absolutePath, '--template', templateFile.absolutePath, '-o', outputFile.absolutePath
+            }
+
+            println "Converted ${file.name} to ${outputFile.name}"
+        }
+    }
+}
diff --git a/doc/scenarios/template.html b/doc/scenarios/template.html
new file mode 100644
index 00000000..1bb16e52
--- /dev/null
+++ b/doc/scenarios/template.html
@@ -0,0 +1,124 @@
+<!doctype html>
+<html $if(lang)$ lang="$lang$" $endif$>
+<head>
+    
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <!--[if lt IE 9]>
+                <script src="http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
+        <![endif]-->
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta http-equiv="Content-Style-Type" content="text/css" />
+
+    <!-- <link rel="stylesheet" type="text/css" href="template.css" /> -->
+    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/template.css" />
+
+    <link href="https://vjs.zencdn.net/5.4.4/video-js.css" rel="stylesheet" />
+
+    <script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
+    <!-- <script type='text/javascript' src='menu/js/jquery.cookie.js'></script> -->
+    <!-- <script type='text/javascript' src='menu/js/jquery.hoverIntent.minified.js'></script> -->
+    <!-- <script type='text/javascript' src='menu/js/jquery.dcjqaccordion.2.7.min.js'></script> -->
+
+    <!-- <link href="menu/css/skins/blue.css" rel="stylesheet" type="text/css" /> -->
+    <!-- <link href="menu/css/skins/graphite.css" rel="stylesheet" type="text/css" /> -->
+    <!-- <link href="menu/css/skins/grey.css" rel="stylesheet" type="text/css" /> -->
+  
+    <!-- <script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> -->
+        
+  
+    <!-- <script src="script.js"></script> -->
+  
+    <!-- <script src="jquery.sticky-kit.js "></script> -->
+    <script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.cookie.js'></script>
+    <script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.hoverIntent.minified.js'></script>
+    <script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.dcjqaccordion.2.7.min.js'></script>
+
+    <link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/blue.css" rel="stylesheet" type="text/css" />
+    <link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/graphite.css" rel="stylesheet" type="text/css" />
+    <link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/grey.css" rel="stylesheet" type="text/css" />
+    <link href="https://cdn.jsdelivr.net/gh/ryangrose/easy-pandoc-templates@948e28e5/css/elegant_bootstrap.css" rel="stylesheet" type="text/css" />
+  
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
+  
+    <script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/script.js"></script>
+  
+    <script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/jquery.sticky-kit.js"></script>
+    <meta name="generator" content="pandoc" />
+$for(author-meta)$
+  <meta name="author" content="$author-meta$" />
+$endfor$
+$if(date-meta)$
+  <meta name="date" content="$date-meta$" />
+$endif$
+  <title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
+  <style type="text/css">code{white-space: pre;}</style>
+$if(quotes)$
+  <style type="text/css">q { quotes: "“" "”" "‘" "’"; }</style>
+$endif$
+$if(highlighting-css)$
+  <style type="text/css">
+$highlighting-css$
+  </style>
+$endif$
+$for(css)$
+  <link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$/>
+$endfor$
+$if(math)$
+  $math$
+$endif$
+$for(header-includes)$
+  $header-includes$
+$endfor$
+</head>
+<body>
+
+    
+  $if(title)$
+  <div class="navbar navbar-static-top">
+    <div class="navbar-inner">
+      <div class="container">
+        <span class="doc-title">$title$</span>
+        <ul class="nav pull-right doc-info">
+          $for(author)$
+          <li><p class="navbar-text">$author$</p></li>
+          $endfor$
+          $if(date)$
+          <li><p class="navbar-text">$date$</p></li>
+          $endif$
+        </ul>
+      </div>
+    </div>
+  </div>
+  $endif$
+  <div class="container">
+    <div class="row">
+      $if(toc)$
+      <div id="$idprefix$TOC" class="span3">
+        <div class="well toc">
+
+        $toc$
+
+        </div>
+      </div>
+      $endif$
+      <div class="span$if(toc)$9$else$12$endif$">
+
+      $if(abstract)$
+          <H1>$abstract-title$</H1>
+          $abstract$
+      $endif$
+
+      $for(include-before)$
+      $include-before$
+      $endfor$
+$body$
+      $for(include-after)$
+      $include-after$
+      $endfor$
+      </div>
+    </div>
+  </div>
+  <script src="https://vjs.zencdn.net/5.4.4/video.js"></script>
+
+</body>
+</html>
diff --git a/etc/jenkinsAgent.Dockerfile b/etc/jenkinsAgent.Dockerfile
index 648e2f8e..f06e9e4f 100644
--- a/etc/jenkinsAgent.Dockerfile
+++ b/etc/jenkinsAgent.Dockerfile
@@ -1,10 +1,6 @@
 FROM eclipse-temurin:21-jdk
 RUN apt-get update && \
-    apt-get install -y bind9-utils && \
+    apt-get install -y bind9-utils pandoc && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
 
-
-# RUN mkdir /opt/app
-# COPY japp.jar /opt
-# CMD ["java", "-jar", "/opt/app/japp.jar"]
diff --git a/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml b/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml
index 3050ab79..724d8ece 100644
--- a/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml
+++ b/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml
@@ -7,7 +7,7 @@ get:
     parameters:
         - $ref: 'auth.yaml#/components/parameters/currentSubject'
         - $ref: 'auth.yaml#/components/parameters/assumedRoles'
-        - name: name
+        - name: iban
           in: query
           required: false
           schema:
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java
index 85e86127..07723a1e 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java
@@ -1,10 +1,14 @@
 package net.hostsharing.hsadminng.hs.office.scenarios;
 
 import net.hostsharing.hsadminng.HsadminNgApplication;
+import net.hostsharing.hsadminng.hs.office.scenarios.contact.AddPhoneNumberToContactData;
+import net.hostsharing.hsadminng.hs.office.scenarios.contact.AmendContactData;
+import net.hostsharing.hsadminng.hs.office.scenarios.contact.RemovePhoneNumberFromContactData;
+import net.hostsharing.hsadminng.hs.office.scenarios.contact.ReplaceContactData;
 import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateExternalDebitorForPartner;
 import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSelfDebitorForPartner;
 import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSepaMandateForDebitor;
-import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteSepaMandateForDebitor;
+import net.hostsharing.hsadminng.hs.office.scenarios.debitor.FinallyDeleteSepaMandateForDebitor;
 import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDebitor;
 import net.hostsharing.hsadminng.hs.office.scenarios.debitor.InvalidateSepaMandateForDebitor;
 import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership;
@@ -43,14 +47,39 @@ class HsOfficeScenarioTests extends ScenarioTest {
 
     @Test
     @Order(1010)
-    @Produces(explicitly = "Partner: Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Board of Directors"})
-    void shouldCreatePartner() {
+    @Produces(explicitly = "Partner: Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Hamburg"})
+    void shouldCreateLegalPersonAsPartner() {
         new CreatePartner(this)
                 .given("partnerNumber", 31010)
                 .given("personType", "LEGAL_PERSON")
                 .given("tradeName", "Test AG")
-                .given("contactCaption", "Test AG - Board of Directors")
-                .given("emailAddress", "board-of-directors@test-ag.example.org")
+                .given("contactCaption", "Test AG - Hamburg")
+                .given("postalAddress", """
+                        Shanghai-Allee 1
+                        20123 Hamburg
+                        """)
+                .given("officePhoneNumber", "+49 40 654321-0")
+                .given("emailAddress", "hamburg@test-ag.example.org")
+                .doRun()
+                .keep();
+    }
+
+    @Test
+    @Order(1011)
+    @Produces(explicitly = "Partner: Michelle Matthieu", implicitly = {"Person: Michelle Matthieu", "Contact: Michelle Matthieu"})
+    void shouldCreateNaturalPersonAsPartner() {
+        new CreatePartner(this)
+                .given("partnerNumber", 31011)
+                .given("personType", "NATURAL_PERSON")
+                .given("givenName", "Michelle")
+                .given("familyName", "Matthieu")
+                .given("contactCaption", "Michelle Matthieu")
+                .given("postalAddress", """
+                        An der Wandse 34
+                        22123 Hamburg
+                        """)
+                .given("officePhoneNumber", "+49 40 123456")
+                .given("emailAddress", "michelle.matthieu@example.org")
                 .doRun()
                 .keep();
     }
@@ -106,6 +135,53 @@ class HsOfficeScenarioTests extends ScenarioTest {
                 .doRun();
     }
 
+    @Test
+    @Order(1100)
+    @Requires("Partner: Michelle Matthieu")
+    void shouldAmendContactData() {
+        new AmendContactData(this)
+                .given("partnerName", "Matthieu")
+                .given("newEmailAddress", "michelle@matthieu.example.org")
+                .doRun();
+    }
+
+    @Test
+    @Order(1101)
+    @Requires("Partner: Michelle Matthieu")
+    void shouldAddPhoneNumberToContactData() {
+        new AddPhoneNumberToContactData(this)
+                .given("partnerName", "Matthieu")
+                .given("phoneNumberKeyToAdd", "mobile")
+                .given("phoneNumberToAdd", "+49 152 1234567")
+                .doRun();
+    }
+
+    @Test
+    @Order(1102)
+    @Requires("Partner: Michelle Matthieu")
+    void shouldRemovePhoneNumberFromContactData() {
+        new RemovePhoneNumberFromContactData(this)
+                .given("partnerName", "Matthieu")
+                .given("phoneNumberKeyToRemove", "office")
+                .doRun();
+    }
+
+    @Test
+    @Order(1103)
+    @Requires("Partner: Test AG")
+    void shouldReplaceContactData() {
+        new ReplaceContactData(this)
+                .given("partnerName", "Test AG")
+                .given("newContactCaption", "Test AG - Norden")
+                .given("newPostalAddress", """
+                        Am Hafen 11
+                        26506 Norden
+                        """)
+                .given("newOfficePhoneNumber", "+49 4931 654321-0")
+                .given("newEmailAddress", "norden@test-ag.example.org")
+                .doRun();
+    }
+
     @Test
     @Order(2010)
     @Requires("Partner: Test AG")
@@ -173,10 +249,18 @@ class HsOfficeScenarioTests extends ScenarioTest {
     @Produces("SEPA-Mandate: Test AG")
     void shouldCreateSepaMandateForDebitor() {
         new CreateSepaMandateForDebitor(this)
-                .given("debitor", "Test AG")
-                .given("memberNumberSuffix", "00")
-                .given("validFrom", "2024-10-15")
-                .given("membershipFeeBillable", "true")
+                // existing debitor
+                .given("debitorNumber", "3101000")
+
+                // new sepa-mandate
+                .given("mandateReference", "Test AG - main debitor")
+                .given("mandateAgreement", "2022-10-12")
+                .given("mandateValidFrom", "2024-10-15")
+
+                // new bank-account
+                .given("bankAccountHolder", "Test AG - debit bank account")
+                .given("bankAccountIBAN", "DE02701500000000594937")
+                .given("bankAccountBIC", "SSKMDEMM")
                 .doRun()
                 .keep();
     }
@@ -186,17 +270,17 @@ class HsOfficeScenarioTests extends ScenarioTest {
     @Requires("SEPA-Mandate: Test AG")
     void shouldInvalidateSepaMandateForDebitor() {
         new InvalidateSepaMandateForDebitor(this)
-                .given("sepaMandateUuid", "%{SEPA-Mandate: Test AG}")
-                .given("validUntil", "2025-09-30")
+                .given("bankAccountIBAN", "DE02701500000000594937")
+                .given("mandateValidUntil", "2025-09-30")
                 .doRun();
     }
 
     @Test
     @Order(3109)
     @Requires("SEPA-Mandate: Test AG")
-    void shouldDeleteSepaMandateForDebitor() {
-        new DeleteSepaMandateForDebitor(this)
-                .given("sepaMandateUuid", "%{SEPA-Mandate: Test AG}")
+    void shouldFinallyDeleteSepaMandateForDebitor() {
+        new FinallyDeleteSepaMandateForDebitor(this)
+                .given("bankAccountIBAN", "DE02701500000000594937")
                 .doRun();
     }
 
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java
new file mode 100644
index 00000000..ffd9df8e
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java
@@ -0,0 +1,23 @@
+package net.hostsharing.hsadminng.hs.office.scenarios;
+
+import net.hostsharing.hsadminng.hs.office.scenarios.UseCase.HttpResponse;
+
+import java.util.function.Consumer;
+
+public class PathAssertion {
+
+    private final String path;
+
+    public PathAssertion(final String path) {
+        this.path = path;
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) {
+        return response -> response.path(path).contains(ScenarioTest.resolve(resolvableValue));
+    }
+
+    public Consumer<HttpResponse> doesNotExist() {
+        return response -> response.path(path).isNull(); // here, null Optional means key not found in JSON
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md
index 36e7c3c8..e8384ba2 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md
@@ -63,4 +63,34 @@ Here, use-cases can be re-used, usually with different data.
 
 ### The Use-Case Itself
 
-The use-case
+The use-case is implemented by the `run()`-method which contains HTTP-calls.
+
+Each HTTP-call is wrapped into either `obtain(...)` to keep the result in a placeholder variable,
+the variable name is also used as a title.
+Or it's wrapped into a `withTitle(...)` to assign a title.
+
+The HTTP-call is followed by some assertions, e.g. the HTTP status and JSON-path-expression-matchers.
+
+Use `${...}` for placeholders which need to be replaced with JSON quotes
+(e.g. strings are quoted, numbers are not),
+`%{...}` for placeholders which need to be rendered raw
+and `&{...}` for placeholders which need to get URI-encoded.
+
+If `???` is added before the closing brace, the property is optional.
+This means, if it's not available in the properties, `null` is used.
+
+Properties with null-values are removed from the JSON.
+If you need to keep a null-value, e.g. to delete a property,
+use `NULL` (all caps) in the template (not the variable value).
+
+A special syntax is the infix `???`-operator like in: `${%{var1???}???%{var2???}%{var3???}}`.
+In this case the first non-null value is used.
+
+
+
+### The Use-Case Verification
+
+The verification-step is implemented by the `verify()`-method which usually contains a HTTP-HTTP-call.
+
+It can also contain a JSON-path verification to check if a certain value is in the result.
+
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java
index efc11c54..4600584a 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java
@@ -12,6 +12,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.TestInfo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.web.server.LocalServerPort;
+import org.testcontainers.shaded.org.apache.commons.lang3.ObjectUtils;
 
 import java.lang.reflect.Method;
 import java.util.HashMap;
@@ -35,7 +36,7 @@ public abstract class ScenarioTest extends ContextBasedTest {
 
         @Override
         public String toString() {
-            return uuid.toString();
+            return ObjectUtils.toString(uuid);
         }
     }
 
@@ -68,7 +69,6 @@ public abstract class ScenarioTest extends ContextBasedTest {
     @AfterEach
     void cleanup() { // final TestInfo testInfo
         properties.clear();
-        // FIXME: Delete all aliases as well to force HTTP GET queries in each scenario?
         testReport.close();
     }
 
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java
index ccb8c96d..aaa8855a 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java
@@ -1,9 +1,57 @@
 package net.hostsharing.hsadminng.hs.office.scenarios;
 
+import org.apache.commons.lang3.StringUtils;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 public class TemplateResolver {
 
+    private final static Pattern pattern = Pattern.compile(",(\\s*})", Pattern.MULTILINE);
+    private static final String IF_NOT_FOUND_SYMBOL = "???";
+
+    enum PlaceholderPrefix {
+        RAW('%') {
+            @Override
+            String convert(final Object value) {
+                return value != null ? value.toString() : "";
+            }
+        },
+        JSON_QUOTED('$'){
+            @Override
+            String convert(final Object value) {
+                return jsonQuoted(value);
+            }
+        },
+        URI_ENCODED('&'){
+            @Override
+            String convert(final Object value) {
+                return value != null ? URLEncoder.encode(value.toString(), StandardCharsets.UTF_8) : "";
+            }
+        };
+
+        private final char prefixChar;
+
+        PlaceholderPrefix(final char prefixChar) {
+            this.prefixChar = prefixChar;
+        }
+
+        static boolean contains(final char givenChar) {
+           return Arrays.stream(values()).anyMatch(p -> p.prefixChar == givenChar);
+        }
+
+        static PlaceholderPrefix ofPrefixChar(final char givenChar) {
+            return Arrays.stream(values()).filter(p -> p.prefixChar == givenChar).findFirst().orElseThrow();
+        }
+
+        abstract String convert(final Object value);
+    }
+
     private final String template;
     private final Map<String, Object> properties;
     private final StringBuilder resolved = new StringBuilder();
@@ -15,18 +63,41 @@ public class TemplateResolver {
     }
 
     String resolve() {
-        copy();
-        return resolved.toString();
+        final var resolved = copy();
+        final var withoutDroppedLines = dropLinesWithNullProperties(resolved);
+        final var result = removeDanglingCommas(withoutDroppedLines);
+        return result;
     }
 
-    private void copy() {
+    private static String removeDanglingCommas(final String withoutDroppedLines) {
+        return pattern.matcher(withoutDroppedLines).replaceAll("$1");
+    }
+
+    private String dropLinesWithNullProperties(final String text) {
+        return Arrays.stream(text.split("\n"))
+                .filter(TemplateResolver::keepLine)
+                .map(TemplateResolver::keptNullValues)
+                .collect(Collectors.joining("\n"));
+    }
+
+    private static boolean keepLine(final String line) {
+        final var trimmed = line.trim();
+        return !trimmed.endsWith("null,") && !trimmed.endsWith("null");
+    }
+
+    private static String keptNullValues(final String line) {
+        return line.replace(": NULL", ": null");
+    }
+
+    private String copy() {
         while (hasMoreChars()) {
-            if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') {
+            if (PlaceholderPrefix.contains(currentChar()) && nextChar() == '{') {
                 startPlaceholder(currentChar());
             } else {
                 resolved.append(fetchChar());
             }
         }
+        return resolved.toString();
     }
 
     private boolean hasMoreChars() {
@@ -41,7 +112,7 @@ public class TemplateResolver {
             if (currentChar() == '}') {
                 --nested;
                 placeholder.append(fetchChar());
-            } else if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') {
+            } else if (PlaceholderPrefix.contains (currentChar()) && nextChar() == '{') {
                 ++nested;
                 placeholder.append(fetchChar());
             } else {
@@ -50,20 +121,33 @@ public class TemplateResolver {
         }
         final var name = new TemplateResolver(placeholder.toString(), properties).resolve();
         final var value = propVal(name);
-        if ( intro == '%') {
-            resolved.append(value);
-        } else {
-            resolved.append(optionallyQuoted(value));
-        }
+        resolved.append(
+                PlaceholderPrefix.ofPrefixChar(intro).convert(value)
+        );
         skipChar('}');
     }
 
-    private Object propVal(final String name) {
-        final var val = properties.get(name);
-        if (val == null) {
-            throw new IllegalStateException("Missing required property: " + name);
+    private Object propVal(final String nameExpression) {
+        if (nameExpression.endsWith(IF_NOT_FOUND_SYMBOL)) {
+            final String pureName = nameExpression.substring(0, nameExpression.length() - IF_NOT_FOUND_SYMBOL.length());
+            return properties.get(pureName);
+        } else if (nameExpression.contains(IF_NOT_FOUND_SYMBOL)) {
+            final var parts = StringUtils.split(nameExpression, IF_NOT_FOUND_SYMBOL);
+            return Arrays.stream(parts).filter(Objects::nonNull).findFirst().orElseGet(() -> {
+                    if ( parts[parts.length-1].isEmpty() ) {
+                        // => whole expression ends with IF_NOT_FOUND_SYMBOL, thus last null element was optional
+                        return null;
+                    }
+                    // => last alternative element in expression was null and not optional
+                    throw new IllegalStateException("Missing required value in property-chain: " + nameExpression);
+            });
+        } else {
+            final var val = properties.get(nameExpression);
+            if (val == null) {
+                throw new IllegalStateException("Missing required property: " + nameExpression);
+            }
+            return val;
         }
-        return val;
     }
 
     private void skipChar(final char expectedChar) {
@@ -104,35 +188,13 @@ public class TemplateResolver {
         return template.charAt(position+1);
     }
 
-    private static String optionallyQuoted(final Object value) {
+    private static String jsonQuoted(final Object value) {
         return switch (value) {
+            case null -> null;
             case Boolean bool -> bool.toString();
             case Number number -> number.toString();
             case String string -> "\"" + string.replace("\n", "\\n") + "\"";
             default -> "\"" + value + "\"";
         };
     }
-
-    public static void main(String[] args) {
-        System.out.println(
-                new TemplateResolver("""
-                        etwas davor,
-                        
-                        ${einfacher Platzhalter},
-                        ${verschachtelter %{Name}},
-                        
-                        und nochmal ohne Quotes:
-                        
-                        %{einfacher Platzhalter},
-                        %{verschachtelter %{Name}},
-                        
-                        etwas danach.
-                        """,
-                        Map.ofEntries(
-                                Map.entry("Name", "placeholder"),
-                                Map.entry("einfacher Platzhalter", "simple placeholder"),
-                                Map.entry("verschachtelter placeholder", "nested placeholder")
-                        )).resolve());
-
-    }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java
new file mode 100644
index 00000000..2d63e204
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java
@@ -0,0 +1,73 @@
+package net.hostsharing.hsadminng.hs.office.scenarios;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class TemplateResolverUnitTest {
+
+    @Test
+    void resolveTemplate() {
+        final var resolved = new TemplateResolver("""
+                    with optional JSON quotes:
+
+                    ${boolean},
+                    ${numeric},
+                    ${simple placeholder},
+                    ${nested %{name}},
+                    ${with-special-chars}
+
+                    and without quotes:
+
+                    %{boolean},
+                    %{numeric},
+                    %{simple placeholder},
+                    %{nested %{name}},
+                    %{with-special-chars}
+
+                    and uri-encoded:
+
+                    &{boolean},
+                    &{numeric},
+                    &{simple placeholder},
+                    &{nested %{name}},
+                    &{with-special-chars}
+                    """,
+                Map.ofEntries(
+                        Map.entry("name", "placeholder"),
+                        Map.entry("boolean", true),
+                        Map.entry("numeric", 42),
+                        Map.entry("simple placeholder", "einfach"),
+                        Map.entry("nested placeholder", "verschachtelt"),
+                        Map.entry("with-special-chars", "3&3 AG")
+                )).resolve();
+
+        assertThat(resolved).isEqualTo("""
+                with optional JSON quotes:
+
+                true,
+                42,
+                "einfach",
+                "verschachtelt",
+                "3&3 AG"
+
+                and without quotes:
+
+                true,
+                42,
+                einfach,
+                verschachtelt,
+                3&3 AG
+
+                and uri-encoded:
+
+                true,
+                42,
+                einfach,
+                verschachtelt,
+                3%263+AG
+                """.trim());
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java
index 02123a14..8aba4319 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java
@@ -57,7 +57,9 @@ public class TestReport {
     }
 
     public void close() {
-        markdownReport.close();
+        if (markdownReport != null) {
+            markdownReport.close();
+        }
     }
 
     private static Object orderNumber(final Method method) {
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java
index 710e4ae1..77aa7b93 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java
@@ -8,6 +8,7 @@ import lombok.Getter;
 import lombok.SneakyThrows;
 import net.hostsharing.hsadminng.reflection.AnnotationFinder;
 import org.apache.commons.collections4.map.LinkedMap;
+import org.assertj.core.api.OptionalAssert;
 import org.hibernate.AssertionFailure;
 import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.Test;
@@ -19,17 +20,21 @@ import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpRequest.BodyPublishers;
 import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.UUID;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
 import static java.net.URLEncoder.encode;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
 import static org.junit.platform.commons.util.StringUtils.isBlank;
 import static org.junit.platform.commons.util.StringUtils.isNotBlank;
 
@@ -80,11 +85,16 @@ public abstract class UseCase<T extends UseCase<?>> {
                     }
                 })
         );
-        return run();
+        final var response = run();
+        verify();
+        return response;
     }
 
     protected abstract HttpResponse run();
 
+    protected void verify() {
+    }
+
     public final UseCase<T> given(final String propName, final Object propValue) {
         givenProperties.put(propName, propValue);
         ScenarioTest.putProperty(propName, propValue);
@@ -101,26 +111,30 @@ public abstract class UseCase<T extends UseCase<?>> {
             final Function<HttpResponse, String> extractor,
             final String... extraInfo) {
         withTitle(ScenarioTest.resolve(alias), () -> {
-            http.get().keep(extractor);
+            final var response = http.get().keep(extractor);
             Arrays.stream(extraInfo).forEach(testReport::printPara);
+            return response;
         });
     }
 
     public final void obtain(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) {
         withTitle(ScenarioTest.resolve(alias), () -> {
-            http.get().keep();
+            final var response = http.get().keep();
             Arrays.stream(extraInfo).forEach(testReport::printPara);
+            return response;
         });
     }
 
-    private void withTitle(final String title, final Runnable code) {
+    public HttpResponse withTitle(final String title, final Supplier<HttpResponse> code) {
         this.nextTitle = title;
-        code.run();
+        final var response = code.get();
         this.nextTitle = null;
+        return response;
     }
 
     @SneakyThrows
-    public final HttpResponse httpGet(final String uriPath) {
+    public final HttpResponse httpGet(final String uriPathWithPlaceholders) {
+        final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
         final var request = HttpRequest.newBuilder()
                 .GET()
                 .uri(new URI("http://localhost:" + testSuite.port + uriPath))
@@ -132,7 +146,8 @@ public abstract class UseCase<T extends UseCase<?>> {
     }
 
     @SneakyThrows
-    public final HttpResponse httpPost(final String uriPath, final JsonTemplate bodyJsonTemplate) {
+    public final HttpResponse httpPost(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
+        final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
         final var requestBody = bodyJsonTemplate.resolvePlaceholders();
         final var request = HttpRequest.newBuilder()
                 .POST(BodyPublishers.ofString(requestBody))
@@ -146,7 +161,8 @@ public abstract class UseCase<T extends UseCase<?>> {
     }
 
     @SneakyThrows
-    public final HttpResponse httpPatch(final String uriPath, final JsonTemplate bodyJsonTemplate) {
+    public final HttpResponse httpPatch(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
+        final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
         final var requestBody = bodyJsonTemplate.resolvePlaceholders();
         final var request = HttpRequest.newBuilder()
                 .method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody))
@@ -160,7 +176,8 @@ public abstract class UseCase<T extends UseCase<?>> {
     }
 
     @SneakyThrows
-    public final HttpResponse httpDelete(final String uriPath) {
+    public final HttpResponse httpDelete(final String uriPathWithPlaceholders) {
+        final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
         final var request = HttpRequest.newBuilder()
                 .DELETE()
                 .uri(new URI("http://localhost:" + testSuite.port + uriPath))
@@ -172,12 +189,27 @@ public abstract class UseCase<T extends UseCase<?>> {
         return new HttpResponse(HttpMethod.DELETE, uriPath, null, response);
     }
 
+    protected PathAssertion path(final String path) {
+        return new PathAssertion(path);
+    }
+
+    protected void verify(
+            final String title,
+            final Supplier<UseCase.HttpResponse> http,
+            final Consumer<UseCase.HttpResponse>... assertions) {
+        withTitle(ScenarioTest.resolve(title), () -> {
+            final var response = http.get();
+            Arrays.stream(assertions).forEach(assertion ->  assertion.accept(response));
+            return response;
+        });
+    }
+
     public final UUID uuid(final String alias) {
         return ScenarioTest.uuid(alias);
     }
 
     public String uriEncoded(final String text) {
-        return encode(ScenarioTest.resolve(text));
+        return encode(ScenarioTest.resolve(text), StandardCharsets.UTF_8);
     }
 
     public static class JsonTemplate {
@@ -193,7 +225,7 @@ public abstract class UseCase<T extends UseCase<?>> {
         }
     }
 
-    public class HttpResponse {
+    public final class HttpResponse {
 
         @Getter
         private final java.net.http.HttpResponse<String> response;
@@ -232,7 +264,7 @@ public abstract class UseCase<T extends UseCase<?>> {
             return this;
         }
 
-        public void keep(final Function<HttpResponse, String> extractor) {
+        public HttpResponse keep(final Function<HttpResponse, String> extractor) {
             final var alias = nextTitle != null ? nextTitle : resultAlias;
             assertThat(alias).as("cannot keep result, no alias found").isNotNull();
 
@@ -240,14 +272,16 @@ public abstract class UseCase<T extends UseCase<?>> {
             ScenarioTest.putAlias(
                     alias,
                     new ScenarioTest.Alias<>(UseCase.this.getClass(), UUID.fromString(value)));
+            return this;
         }
 
-        public void keep() {
+        public HttpResponse keep() {
             final var alias = nextTitle != null ? nextTitle : resultAlias;
             assertThat(alias).as("cannot keep result, no alias found").isNotNull();
             ScenarioTest.putAlias(
                     alias,
                     new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid));
+            return this;
         }
 
         @SneakyThrows
@@ -263,7 +297,21 @@ public abstract class UseCase<T extends UseCase<?>> {
 
         @SneakyThrows
         public String getFromBody(final String path) {
-            return JsonPath.parse(response.body()).read(path);
+            return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path));
+        }
+
+        @SneakyThrows
+        public Optional<String> getFromBodyAsOptional(final String path) {
+            try {
+                return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path)));
+            } catch (final Exception e) {
+                return null; // means the property did not exist at all, not that it was there with value null
+            }
+        }
+
+        @SneakyThrows
+        public OptionalAssert<String> path(final String path) {
+            return assertThat(getFromBodyAsOptional(path));
         }
 
         @SneakyThrows
@@ -274,6 +322,8 @@ public abstract class UseCase<T extends UseCase<?>> {
                 testReport.printLine("\n### " + nextTitle + "\n");
             } else if (resultAlias != null) {
                 testReport.printLine("\n### " + resultAlias + "\n");
+            } else {
+                fail("please wrap the http...-call in the UseCase using `withTitle(...)`");
             }
 
             // the request
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java
new file mode 100644
index 00000000..d21d6413
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java
@@ -0,0 +1,16 @@
+package net.hostsharing.hsadminng.hs.office.scenarios;
+
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+public class UseCaseNotImplementedYet extends UseCase<UseCaseNotImplementedYet> {
+
+    public UseCaseNotImplementedYet(final ScenarioTest testSuite) {
+        super(testSuite);
+    }
+
+    @Override
+    protected HttpResponse run() {
+        assumeThat(false).isTrue(); // makes the test gray
+        return null;
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java
new file mode 100644
index 00000000..3e837195
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java
@@ -0,0 +1,51 @@
+package net.hostsharing.hsadminng.hs.office.scenarios.contact;
+
+import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
+import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
+import org.springframework.http.HttpStatus;
+
+
+import static io.restassured.http.ContentType.JSON;
+import static org.springframework.http.HttpStatus.OK;
+
+public class AddPhoneNumberToContactData extends UseCase<AddPhoneNumberToContactData> {
+
+    public AddPhoneNumberToContactData(final ScenarioTest testSuite) {
+        super(testSuite);
+    }
+
+    @Override
+    protected HttpResponse run() {
+
+        obtain(
+                "partnerContactUuid",
+                () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
+                        .expecting(OK).expecting(JSON),
+                response -> response.expectArrayElements(1).getFromBody("[0].contact.uuid"),
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+        );
+
+        withTitle("Patch the Additional Phone-Number into the Contact", () ->
+                httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
+                {
+                    "phoneNumbers": {
+                        ${phoneNumberKeyToAdd}: ${phoneNumberToAdd}
+                    }
+                }
+                """))
+                .expecting(HttpStatus.OK)
+        );
+
+        return null;
+    }
+
+    @Override
+    protected void verify() {
+        verify(
+                "Verify if the New Phone Number Got Added",
+                () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
+                        .expecting(OK).expecting(JSON).expectArrayElements(1),
+                path("[0].contact.phoneNumbers.%{phoneNumberKeyToAdd}").contains("%{phoneNumberToAdd}")
+        );
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java
new file mode 100644
index 00000000..8f45d83a
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java
@@ -0,0 +1,44 @@
+package net.hostsharing.hsadminng.hs.office.scenarios.contact;
+
+import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
+import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
+import org.springframework.http.HttpStatus;
+
+import static io.restassured.http.ContentType.JSON;
+import static org.springframework.http.HttpStatus.OK;
+
+public class AmendContactData extends UseCase<AmendContactData> {
+
+    public AmendContactData(final ScenarioTest testSuite) {
+        super(testSuite);
+    }
+
+    @Override
+    protected HttpResponse run() {
+
+        obtain("partnerContactUuid", () ->
+                httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
+                        .expecting(OK).expecting(JSON),
+                response -> response.expectArrayElements(1).getFromBody("[0].contact.uuid"),
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+        );
+
+        withTitle("Patch the New Phone Number Into the Contact", () ->
+            httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
+            {
+                "caption": ${newContactCaption???},
+                "postalAddress": ${newPostalAddress???},
+                "phoneNumbers": {
+                    "office": ${newOfficePhoneNumber???}
+                },
+                "emailAddresses": {
+                    "main": ${newEmailAddress???}
+                }
+            }
+            """))
+            .expecting(HttpStatus.OK)
+        );
+
+        return null;
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java
new file mode 100644
index 00000000..c499ee71
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java
@@ -0,0 +1,50 @@
+package net.hostsharing.hsadminng.hs.office.scenarios.contact;
+
+import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
+import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
+import org.springframework.http.HttpStatus;
+
+import static io.restassured.http.ContentType.JSON;
+import static org.springframework.http.HttpStatus.OK;
+
+public class RemovePhoneNumberFromContactData extends UseCase<RemovePhoneNumberFromContactData> {
+
+    public RemovePhoneNumberFromContactData(final ScenarioTest testSuite) {
+        super(testSuite);
+    }
+
+    @Override
+    protected HttpResponse run() {
+
+        obtain(
+                "partnerContactUuid",
+                () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
+                        .expecting(OK).expecting(JSON),
+                response -> response.expectArrayElements(1).getFromBody("[0].contact.uuid"),
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+        );
+
+        withTitle("Patch the Additional Phone-Number into the Contact", () ->
+                httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
+                {
+                    "phoneNumbers": {
+                        ${phoneNumberKeyToRemove}: NULL
+                    }
+                }
+                """))
+                .expecting(HttpStatus.OK)
+        );
+
+        return null;
+    }
+
+    @Override
+    protected void verify() {
+        verify(
+                "Verify if the New Phone Number Got Added",
+                () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
+                        .expecting(OK).expecting(JSON).expectArrayElements(1),
+                path("[0].contact.phoneNumbers.%{phoneNumberKeyToRemove}").doesNotExist()
+        );
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java
new file mode 100644
index 00000000..b4916dda
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java
@@ -0,0 +1,64 @@
+package net.hostsharing.hsadminng.hs.office.scenarios.contact;
+
+import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
+import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
+
+import static io.restassured.http.ContentType.JSON;
+import static org.springframework.http.HttpStatus.CREATED;
+import static org.springframework.http.HttpStatus.OK;
+
+public class ReplaceContactData extends UseCase<ReplaceContactData> {
+
+    public ReplaceContactData(final ScenarioTest testSuite) {
+        super(testSuite);
+    }
+
+    @Override
+    protected HttpResponse run() {
+
+        obtain("partnerRelationUuid", () ->
+                httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
+                    .expecting(OK).expecting(JSON),
+                response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+        );
+
+        obtain("Contact: %{newContactCaption}", () ->
+            httpPost("/api/hs/office/contacts", usingJsonBody("""
+                {
+                    "caption": ${newContactCaption},
+                    "postalAddress": ${newPostalAddress???},
+                    "phoneNumbers": {
+                        "phone": ${newOfficePhoneNumber???}
+                    },
+                    "emailAddresses": {
+                        "main": ${newEmailAddress???}
+                    }
+                }
+                """))
+                .expecting(CREATED).expecting(JSON),
+            "Please check first if that contact already exists, if so, use it's UUID below."
+        );
+
+        withTitle("Replace the Contact-Reference in the Partner-Relation", () ->
+            httpPatch("/api/hs/office/relations/%{partnerRelationUuid}", usingJsonBody("""
+                    {
+                        "contactUuid": ${Contact: %{newContactCaption}}
+                    }
+                    """))
+            .expecting(OK)
+        );
+
+        return null;
+    }
+
+    @Override
+    protected void verify() {
+        verify(
+                "Verify if the Contact-Relation Got Replaced in the Partner-Relation",
+                () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
+                        .expecting(OK).expecting(JSON).expectArrayElements(1),
+                path("[0].contact.caption").contains("%{newContactCaption}")
+        );
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java
index e9afdcc2..194d4513 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java
@@ -26,7 +26,7 @@ public class CreateExternalDebitorForPartner extends UseCase<CreateExternalDebit
                         httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}"))
                                 .expecting(OK).expecting(JSON),
                 response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
-                "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
         );
 
         obtain("BankAccount: Billing GmbH - refund bank account", () ->
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java
index 91a21a00..fc16adb3 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java
@@ -19,8 +19,7 @@ public class CreateSelfDebitorForPartner extends UseCase<CreateSelfDebitorForPar
                 httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerPersonTradeName}"))
                         .expecting(OK).expecting(JSON),
                 response -> response.expectArrayElements(1).getFromBody("[0].holder.uuid"),
-                "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one.",
-                "**HINT**: With production data, you might get multiple results and have to decide which is the right one."
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
         );
 
         obtain("BankAccount: Test AG - refund bank account", () ->
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java
index 9bbba7a6..606e2320 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java
@@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
 
 import static io.restassured.http.ContentType.JSON;
 import static org.springframework.http.HttpStatus.CREATED;
+import static org.springframework.http.HttpStatus.OK;
 
 public class CreateSepaMandateForDebitor extends UseCase<CreateSepaMandateForDebitor> {
 
@@ -14,12 +15,19 @@ public class CreateSepaMandateForDebitor extends UseCase<CreateSepaMandateForDeb
 
     @Override
     protected HttpResponse run() {
+
+        obtain("Debitor: Test AG - main debitor", () ->
+            httpGet("/api/hs/office/debitors?debitorNumber=&{debitorNumber}")
+                    .expecting(OK).expecting(JSON),
+            response -> response.expectArrayElements(1).getFromBody("[0].uuid")
+        );
+
         obtain("BankAccount: Test AG - debit bank account", () ->
             httpPost("/api/hs/office/bankaccounts", usingJsonBody("""
                      {
-                         "holder": "Test AG - debit bank account",
-                         "iban": "DE02701500000000594937",
-                         "bic": "SSKMDEMM"
+                         "holder": ${bankAccountHolder},
+                         "iban": ${bankAccountIBAN},
+                         "bic": ${bankAccountBIC}
                     }
                     """))
                     .expecting(CREATED).expecting(JSON)
@@ -29,9 +37,9 @@ public class CreateSepaMandateForDebitor extends UseCase<CreateSepaMandateForDeb
                 {
                    "debitorUuid": ${Debitor: Test AG - main debitor},
                    "bankAccountUuid": ${BankAccount: Test AG - debit bank account},
-                   "reference": "Test AG - main debitor",
-                   "agreement": "2022-10-12",
-                   "validFrom": "2022-10-13"
+                   "reference": ${mandateReference},
+                   "agreement": ${mandateAgreement},
+                   "validFrom": ${mandateValidFrom}
                 }
                 """))
                 .expecting(CREATED).expecting(JSON);
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteDebitor.java
index 016f1a75..19a0f159 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteDebitor.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteDebitor.java
@@ -24,8 +24,10 @@ public class DeleteDebitor extends UseCase<DeleteDebitor> {
 
     @Override
     protected HttpResponse run() {
-        httpDelete("/api/hs/office/debitors/" + uuid("Debitor: Test AG - delete debitor"))
-                .expecting(HttpStatus.NO_CONTENT);
+        withTitle("Delete the Debitor using its UUID", () ->
+            httpDelete("/api/hs/office/debitors/&{Debitor: Test AG - delete debitor}")
+                .expecting(HttpStatus.NO_CONTENT)
+        );
         return null;
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteSepaMandateForDebitor.java
deleted file mode 100644
index e5c9b94a..00000000
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteSepaMandateForDebitor.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
-
-import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
-import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
-import org.springframework.http.HttpStatus;
-
-
-public class DeleteSepaMandateForDebitor extends UseCase<DeleteSepaMandateForDebitor> {
-
-    public DeleteSepaMandateForDebitor(final ScenarioTest testSuite) {
-        super(testSuite);
-    }
-
-    @Override
-    protected HttpResponse run() {
-        httpDelete("/api/hs/office/sepamandates/" + uuid("SEPA-Mandate: Test AG"))
-                .expecting(HttpStatus.NO_CONTENT);
-        return null;
-    }
-}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java
index 82aae503..e5459c88 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java
@@ -12,7 +12,7 @@ public class DontDeleteDefaultDebitor extends UseCase<DontDeleteDefaultDebitor>
 
     @Override
     protected HttpResponse run() {
-        httpDelete("/api/hs/office/debitors/" + uuid("Debitor: Test AG - main debitor"))
+        httpDelete("/api/hs/office/debitors/&{Debitor: Test AG - main debitor}")
                 // TODO.spec: should be CONFLICT or CLIENT_ERROR for Debitor "00"  - but how to delete Partners?
                 .expecting(HttpStatus.NO_CONTENT);
         return null;
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java
new file mode 100644
index 00000000..224a98f3
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java
@@ -0,0 +1,31 @@
+package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
+
+import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
+import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
+import org.springframework.http.HttpStatus;
+
+import static io.restassured.http.ContentType.JSON;
+import static org.springframework.http.HttpStatus.OK;
+
+public class FinallyDeleteSepaMandateForDebitor extends UseCase<FinallyDeleteSepaMandateForDebitor> {
+
+    public FinallyDeleteSepaMandateForDebitor(final ScenarioTest testSuite) {
+        super(testSuite);
+    }
+
+    @Override
+    protected HttpResponse run() {
+
+        obtain("SEPA-Mandate: %{bankAccountIBAN}", () ->
+                        httpGet("/api/hs/office/sepamandates?iban=&{bankAccountIBAN}")
+                                .expecting(OK).expecting(JSON),
+                response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
+                "With production data, the bank-account could be used in multiple SEPA-mandates, make sure to use the right one!"
+        );
+
+        // TODO.spec: When to allow actual deletion of SEPA-mandates? Add constraint accordingly.
+        return withTitle("Delete the SEPA-Mandate by its UUID", () -> httpDelete("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}")
+                .expecting(HttpStatus.NO_CONTENT)
+        );
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java
index 9d13706e..3af160c7 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java
@@ -15,11 +15,20 @@ public class InvalidateSepaMandateForDebitor extends UseCase<InvalidateSepaManda
     @Override
     protected HttpResponse run() {
 
-        return httpPatch("/api/hs/office/sepamandates/" + uuid("SEPA-Mandate: Test AG"), usingJsonBody("""
+        obtain("SEPA-Mandate: %{bankAccountIBAN}", () ->
+                httpGet("/api/hs/office/sepamandates?iban=&{bankAccountIBAN}")
+                        .expecting(OK).expecting(JSON),
+                response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
+                "With production data, the bank-account could be used in multiple SEPA-mandates, make sure to use the right one!"
+        );
+
+        return withTitle("Patch the End of the Mandate into the SEPA-Mandate", () ->
+                httpPatch("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}", usingJsonBody("""
                 {
-                   "validUntil": ${validUntil}
+                   "validUntil": ${mandateValidUntil}
                 }
                 """))
-                .expecting(OK).expecting(JSON);
+                .expecting(OK).expecting(JSON)
+        );
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java
index 6e41ce76..6c1fd1dd 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java
@@ -22,7 +22,7 @@ public class AddOperationsContactToPartner extends UseCase<AddOperationsContactT
                         httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}"))
                                 .expecting(OK).expecting(JSON),
                 response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
-                "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
         );
 
         obtain("Person: %{operationsContactGivenName} %{operationsContactFamilyName}", () ->
@@ -64,4 +64,15 @@ public class AddOperationsContactToPartner extends UseCase<AddOperationsContactT
                 """))
                 .expecting(CREATED).expecting(JSON);
     }
+
+    @Override
+    protected void verify() {
+        verify(
+                "Verify the New OPERATIONS Relation",
+                () -> httpGet("/api/hs/office/relations?relationType=OPERATIONS&personData=" + uriEncoded(
+                        "%{operationsContactFamilyName}"))
+                        .expecting(OK).expecting(JSON).expectArrayElements(1),
+                path("[0].contact.caption").contains("%{operationsContactGivenName} %{operationsContactFamilyName}")
+        );
+    }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java
index cb5c8136..1914cbb3 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java
@@ -22,7 +22,7 @@ public class AddRepresentativeToPartner extends UseCase<AddRepresentativeToPartn
                 httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}"))
                         .expecting(OK).expecting(JSON),
                 response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
-                "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
         );
 
         obtain("Person: %{representativeGivenName} %{representativeFamilyName}", () ->
@@ -65,4 +65,14 @@ public class AddRepresentativeToPartner extends UseCase<AddRepresentativeToPartn
                 """))
                 .expecting(CREATED).expecting(JSON);
     }
+
+    @Override
+    protected void verify() {
+        verify(
+                "Verify the REPRESENTATIVE Relation Got Removed",
+                () -> httpGet("/api/hs/office/relations?relationType=REPRESENTATIVE&personData=" + uriEncoded("%{representativeFamilyName}"))
+                        .expecting(OK).expecting(JSON).expectArrayElements(1),
+                path("[0].contact.caption").contains("%{representativeGivenName} %{representativeFamilyName}")
+        );
+    }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java
index c96acbdf..eb7e7d59 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java
@@ -28,22 +28,28 @@ public class CreatePartner extends UseCase<CreatePartner> {
                 "Even in production data we expect this query to return just a single result." // TODO.impl: add constraint?
         );
 
-        obtain("Person: %{tradeName}", () ->
+        obtain("Person: %{%{tradeName???}???%{givenName???} %{familyName???}}", () ->
             httpPost("/api/hs/office/persons", usingJsonBody("""
                     {
-                        "personType": ${personType},
-                        "tradeName": ${tradeName}
+                        "personType": ${personType???},
+                        "tradeName": ${tradeName???},
+                        "givenName": ${givenName???},
+                        "familyName": ${familyName???}
                     }
                     """))
                     .expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
         );
 
-        obtain("Contact: %{tradeName} - Board of Directors", () ->
+        obtain("Contact: %{contactCaption}", () ->
             httpPost("/api/hs/office/contacts", usingJsonBody("""
                     {
                         "caption": ${contactCaption},
+                        "postalAddress": ${postalAddress???},
+                        "phoneNumbers": {
+                            "office": ${officePhoneNumber???}
+                        },
                         "emailAddresses": {
-                            "main": ${emailAddress}
+                            "main": ${emailAddress???}
                         }
                     }
                     """))
@@ -55,8 +61,8 @@ public class CreatePartner extends UseCase<CreatePartner> {
                     "partnerNumber": ${partnerNumber},
                     "partnerRel": {
                          "anchorUuid": ${Person: Hostsharing eG},
-                         "holderUuid": ${Person: %{tradeName}},
-                         "contactUuid": ${Contact: %{tradeName} - Board of Directors}
+                         "holderUuid": ${Person: %{%{tradeName???}???%{givenName???} %{familyName???}}},
+                         "contactUuid": ${Contact: %{contactCaption}}
                     },
                     "details": {
                         "registrationOffice": "Registergericht Hamburg",
@@ -66,4 +72,13 @@ public class CreatePartner extends UseCase<CreatePartner> {
                 """))
                 .expecting(HttpStatus.CREATED).expecting(ContentType.JSON);
     }
+
+    @Override
+    protected void verify() {
+        verify(
+                "Verify the New Partner Relation",
+                () -> httpGet("/api/hs/office/relations?relationType=PARTNER&contactData=&{contactCaption}")
+                        .expecting(OK).expecting(JSON).expectArrayElements(1)
+        );
+    }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java
index ae24dfd1..4453d959 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java
@@ -18,8 +18,10 @@ public class DeletePartner extends UseCase<DeletePartner> {
 
     @Override
     protected HttpResponse run() {
-        httpDelete("/api/hs/office/partners/" + uuid("Partner: Delete AG"))
-                .expecting(HttpStatus.NO_CONTENT);
+        withTitle("Delete Partner by its UUID", () ->
+            httpDelete("/api/hs/office/partners/&{Partner: Delete AG}")
+                .expecting(HttpStatus.NO_CONTENT)
+        );
         return null;
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java
index 56db97e8..4a43503c 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java
@@ -14,12 +14,14 @@ public class CreatePerson extends UseCase<CreatePerson> {
     @Override
     protected HttpResponse run() {
 
-        return httpPost("/api/hs/office/persons", usingJsonBody("""
+        return withTitle("Create the Person", () ->
+                httpPost("/api/hs/office/persons", usingJsonBody("""
                     {
                         "personType": ${personType},
                         "tradeName": ${tradeName}
                     }
                     """))
-                    .expecting(HttpStatus.CREATED).expecting(ContentType.JSON);
+                    .expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
+        );
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java
index 64584075..c30019e7 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java
@@ -1,9 +1,10 @@
 package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
 
-import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
 import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
+import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
 
 import static io.restassured.http.ContentType.JSON;
+import static org.springframework.http.HttpStatus.NOT_FOUND;
 import static org.springframework.http.HttpStatus.NO_CONTENT;
 import static org.springframework.http.HttpStatus.OK;
 
@@ -16,14 +17,27 @@ public class RemoveOperationsContactFromPartner extends UseCase<RemoveOperations
     @Override
     protected HttpResponse run() {
 
-        obtain("Operations-Contact: %{operationsContactPerson}", () ->
-                        httpGet("/api/hs/office/relations?relationType=OPERATIONS&name=" + uriEncoded("%{operationsContactPerson}"))
+        obtain("Operations-Contact: %{operationsContactPerson}",
+                () ->
+                        httpGet("/api/hs/office/relations?relationType=OPERATIONS&name=" + uriEncoded(
+                                "%{operationsContactPerson}"))
                                 .expecting(OK).expecting(JSON),
                 response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
-                "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
         );
 
-        return httpDelete("/api/hs/office/relations/" + uuid("Operations-Contact: %{operationsContactPerson}"))
-                .expecting(NO_CONTENT);
+        return withTitle("Delete the Contact", () ->
+                httpDelete("/api/hs/office/relations/&{Operations-Contact: %{operationsContactPerson}}")
+                        .expecting(NO_CONTENT)
+        );
+    }
+
+    @Override
+    protected void verify() {
+        verify(
+                "Verify the New OPERATIONS Relation",
+                () -> httpGet("/api/hs/office/relations/&{Operations-Contact: %{operationsContactPerson}}")
+                        .expecting(NOT_FOUND)
+        );
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java
index 3c84603f..3e4ae74b 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java
@@ -22,7 +22,7 @@ public class SubscribeToMailinglist extends UseCase<SubscribeToMailinglist> {
                         httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}"))
                                 .expecting(OK).expecting(JSON),
                 response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
-                "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
         );
 
         obtain("Person: %{subscriberGivenName} %{subscriberFamilyName}", () ->
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java
index 6f059902..dfb17ea1 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java
@@ -22,10 +22,12 @@ public class UnsubscribeFromMailinglist extends UseCase<UnsubscribeFromMailingli
                             "&contactData=" + uriEncoded("%{subscriberEMailAddress}"))
                         .expecting(OK).expecting(JSON),
                 response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
-                "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
+                "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
         );
 
-        return httpDelete("/api/hs/office/relations/" + uuid("Subscription: %{subscriberEMailAddress}"))
-                .expecting(NO_CONTENT);
+        return withTitle("Delete the Subscriber-Relation by its UUID", () ->
+                httpDelete("/api/hs/office/relations/&{Subscription: %{subscriberEMailAddress}}")
+                .expecting(NO_CONTENT)
+        );
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java
index 89b25f35..48ed0d9c 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java
@@ -8,7 +8,6 @@ import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountReposi
 import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
-import org.json.JSONException;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
@@ -58,7 +57,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
     class ListSepaMandates {
 
         @Test
-        void globalAdmin_canViewAllSepaMandates_ifNoCriteriaGiven() throws JSONException {
+        void globalAdmin_canViewAllSepaMandates_ifNoCriteriaGiven() {
 
             RestAssured // @formatter:off
                 .given()
@@ -97,6 +96,36 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
                     """));
                 // @formatter:on
         }
+
+        @Test
+        void globalAdmin_canFindSepaMandateByName() {
+
+            RestAssured // @formatter:off
+                    .given()
+                        .header("current-subject", "superuser-alex@hostsharing.net")
+                        .port(port)
+                    .when()
+                        .get("http://localhost/api/hs/office/sepamandates?iban=DE02120300000000202051")
+                    .then().log().all().assertThat()
+                        .statusCode(200)
+                        .contentType("application/json")
+                        .log().all()
+                        .body("", lenientlyEquals("""
+                        [
+                             {
+                                 "debitor": { "debitorNumber": 1000111 },
+                                 "bankAccount": {
+                                    "iban": "DE02120300000000202051",
+                                    "holder": "First GmbH"
+                                 },
+                                 "reference": "ref-10001-11",
+                                 "validFrom": "2022-10-01",
+                                 "validTo": "2026-12-31"
+                             }
+                         ]
+                        """));
+                        // @formatter:on
+        }
     }
 
     @Nested
diff --git a/src/main/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java b/src/test/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java
similarity index 100%
rename from src/main/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java
rename to src/test/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java