gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: Bank refactoring.


From: gnunet
Subject: [libeufin] branch master updated: Bank refactoring.
Date: Mon, 11 Sep 2023 15:37:31 +0200

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new 16fa54c9 Bank refactoring.
16fa54c9 is described below

commit 16fa54c96f3d9600748ea8b0530fd8ad4b146319
Author: MS <ms@taler.net>
AuthorDate: Mon Sep 11 15:08:46 2023 +0200

    Bank refactoring.
    
    Deleting entirely the previous Sandbox tree and
    creating a new one with only one libeufin-bank.
---
 .idea/$PRODUCT_WORKSPACE_FILE$                     |   19 -
 .idea/.gitignore                                   |    3 -
 .idea/codeStyles/Project.xml                       |   11 -
 .idea/codeStyles/codeStyleConfig.xml               |    5 -
 .idea/dictionaries/dold.xml                        |   25 -
 .idea/gradle.xml                                   |    6 +-
 .idea/inspectionProfiles/Project_Default.xml       |    9 -
 .idea/kotlinc.xml                                  |    3 -
 .idea/libraries-with-intellij-classes.xml          |   65 -
 .idea/misc.xml                                     |   10 +-
 .idea/runConfigurations/SchedulingTest.xml         |   21 -
 .idea/runConfigurations/run_sandbox.xml            |   21 -
 .idea/runConfigurations/test_nexus.xml             |   21 -
 .idea/runConfigurations/test_sandbox.xml           |   23 -
 .idea/uiDesigner.xml                               |  124 --
 .idea/workspace.xml                                |  231 +++
 bank/build.gradle                                  |    3 +
 .../main/kotlin/tech/libeufin/bank/CircuitApi.kt   |  841 ---------
 .../kotlin/tech/libeufin/bank/ConversionService.kt |  433 -----
 bank/src/main/kotlin/tech/libeufin/bank/DB.kt      |  747 --------
 .../src/main/kotlin/tech/libeufin/bank/Database.kt |  266 ++-
 .../tech/libeufin/bank/EbicsProtocolBackend.kt     | 1436 ----------------
 bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt |  472 -----
 bank/src/main/kotlin/tech/libeufin/bank/JSON.kt    |  154 --
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    | 1801 ++------------------
 .../kotlin/tech/libeufin/bank/XMLEbicsConverter.kt |   70 -
 .../main/kotlin/tech/libeufin/bank/bankAccount.kt  |  276 ---
 bank/src/main/resources/logback.xml                |    2 +-
 bank/src/test/kotlin/BalanceTest.kt                |  115 --
 bank/src/test/kotlin/DBTest.kt                     |  152 --
 bank/src/test/kotlin/DatabaseTest.kt               |   90 +-
 bank/src/test/kotlin/EbicsErrorTest.kt             |   24 -
 bank/src/test/kotlin/LibeuFinApiTest.kt            |   26 +
 bank/src/test/kotlin/StringsTest.kt                |   37 -
 database-versioning/new/libeufin-bank-0001.sql     |   47 +-
 database-versioning/new/procedures.sql             |   38 +-
 nexus/build.gradle                                 |    5 +-
 nexus/src/test/kotlin/ConversionServiceTest.kt     |  395 -----
 nexus/src/test/kotlin/DbEventTest.kt               |   71 -
 nexus/src/test/kotlin/EbicsTest.kt                 |  383 -----
 nexus/src/test/kotlin/Iso20022Test.kt              |  204 ---
 nexus/src/test/kotlin/JsonTest.kt                  |  109 --
 nexus/src/test/kotlin/LetterFormatTest.kt          |   25 -
 nexus/src/test/kotlin/MakeEnv.kt                   |  772 ---------
 nexus/src/test/kotlin/NexusApiTest.kt              |  272 ---
 nexus/src/test/kotlin/PainTest.kt                  |   33 -
 nexus/src/test/kotlin/PostFinance.kt               |  158 --
 nexus/src/test/kotlin/SandboxAccessApiTest.kt      |  491 ------
 nexus/src/test/kotlin/SandboxBankAccountTest.kt    |   73 -
 nexus/src/test/kotlin/SandboxCircuitApiTest.kt     |  662 -------
 nexus/src/test/kotlin/SandboxLegacyApiTest.kt      |  192 ---
 nexus/src/test/kotlin/SchedulingTest.kt            |  179 --
 nexus/src/test/kotlin/SplitString.kt               |   14 -
 nexus/src/test/kotlin/SubjectNormalization.kt      |   36 -
 nexus/src/test/kotlin/TalerTest.kt                 |  260 ---
 nexus/src/test/kotlin/XLibeufinBankTest.kt         |  159 --
 nexus/src/test/kotlin/XPathTest.kt                 |   42 -
 .../camt.053/de.camt.053.001.02.xml                |  488 ------
 nexus/src/test/resources/logback-test.xml          |   28 -
 util/build.gradle                                  |    3 -
 util/src/main/kotlin/Config.kt                     |    5 +-
 util/src/main/kotlin/DB.kt                         |    6 +-
 util/src/main/kotlin/HTTP.kt                       |   34 +-
 util/src/main/kotlin/iban.kt                       |    4 +-
 util/src/main/kotlin/startServer.kt                |    2 +
 util/src/main/kotlin/time.kt                       |    2 +
 util/src/test/kotlin/StartServerTest.kt            |   32 -
 67 files changed, 750 insertions(+), 12016 deletions(-)

diff --git a/.idea/$PRODUCT_WORKSPACE_FILE$ b/.idea/$PRODUCT_WORKSPACE_FILE$
deleted file mode 100644
index a1409e4b..00000000
--- a/.idea/$PRODUCT_WORKSPACE_FILE$
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="masterDetails">
-    <states>
-      <state key="ProjectJDKs.UI">
-        <settings>
-          <last-edited>12</last-edited>
-          <splitter-proportions>
-            <option name="proportions">
-              <list>
-                <option value="0.2" />
-              </list>
-            </option>
-          </splitter-proportions>
-        </settings>
-      </state>
-    </states>
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 0e40fe8f..00000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-
-# Default ignored files
-/workspace.xml
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
deleted file mode 100644
index 1eb52aaa..00000000
--- a/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<component name="ProjectCodeStyleConfiguration">
-  <code_scheme name="Project" version="173">
-    <JetCodeStyleSettings>
-      <option name="SPACE_AROUND_RANGE" value="true" />
-      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
-    </JetCodeStyleSettings>
-    <codeStyleSettings language="kotlin">
-      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
-    </codeStyleSettings>
-  </code_scheme>
-</component>
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml 
b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123c..00000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<component name="ProjectCodeStyleConfiguration">
-  <state>
-    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
-  </state>
-</component>
\ No newline at end of file
diff --git a/.idea/dictionaries/dold.xml b/.idea/dictionaries/dold.xml
deleted file mode 100644
index d6ddbaa2..00000000
--- a/.idea/dictionaries/dold.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<component name="ProjectDictionaryState">
-  <dictionary name="dold">
-    <words>
-      <w>affero</w>
-      <w>camt</w>
-      <w>combinators</w>
-      <w>crdt</w>
-      <w>cronspec</w>
-      <w>dbit</w>
-      <w>ebics</w>
-      <w>gnunet</w>
-      <w>iban</w>
-      <w>infos</w>
-      <w>keyletter</w>
-      <w>libeufin</w>
-      <w>payto</w>
-      <w>pdng</w>
-      <w>servicer</w>
-      <w>sqlite</w>
-      <w>taler</w>
-      <w>talerwiregateway</w>
-      <w>wtid</w>
-    </words>
-  </dictionary>
-</component>
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index f5d0571c..e1d33bca 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,16 +4,12 @@
   <component name="GradleSettings">
     <option name="linkedExternalProjectsSettings">
       <GradleProjectSettings>
-        <option name="delegatedBuild" value="true" />
-        <option name="testRunner" value="GRADLE" />
         <option name="distributionType" value="DEFAULT_WRAPPED" />
         <option name="externalProjectPath" value="$PROJECT_DIR$" />
-        <option name="gradleJvm" value="16" />
         <option name="modules">
           <set>
             <option value="$PROJECT_DIR$" />
-            <option value="$PROJECT_DIR$/nexus" />
-            <option value="$PROJECT_DIR$/sandbox" />
+            <option value="$PROJECT_DIR$/bank" />
             <option value="$PROJECT_DIR$/util" />
           </set>
         </option>
diff --git a/.idea/inspectionProfiles/Project_Default.xml 
b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index c29fcc6b..00000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<component name="InspectionProjectProfileManager">
-  <profile version="1.0">
-    <option name="myName" value="Project Default" />
-    <inspection_tool class="FoldInitializerAndIfToElvis" enabled="false" 
level="INFO" enabled_by_default="false" />
-    <inspection_tool class="JsonStandardCompliance" enabled="true" 
level="ERROR" enabled_by_default="true">
-      <option name="myWarnAboutComments" value="false" />
-    </inspection_tool>
-  </profile>
-</component>
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 059e602f..4251b727 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,8 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="Kotlin2JvmCompilerArguments">
-    <option name="jvmTarget" value="1.8" />
-  </component>
   <component name="KotlinJpsPluginSettings">
     <option name="version" value="1.7.22" />
   </component>
diff --git a/.idea/libraries-with-intellij-classes.xml 
b/.idea/libraries-with-intellij-classes.xml
deleted file mode 100644
index 9fa31567..00000000
--- a/.idea/libraries-with-intellij-classes.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="libraries-with-intellij-classes">
-    <option name="intellijApiContainingLibraries">
-      <list>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="ideaIU" />
-          <option name="groupId" value="com.jetbrains.intellij.idea" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="ideaIU" />
-          <option name="groupId" value="com.jetbrains" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="ideaIC" />
-          <option name="groupId" value="com.jetbrains.intellij.idea" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="ideaIC" />
-          <option name="groupId" value="com.jetbrains" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="pycharmPY" />
-          <option name="groupId" value="com.jetbrains.intellij.pycharm" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="pycharmPY" />
-          <option name="groupId" value="com.jetbrains" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="pycharmPC" />
-          <option name="groupId" value="com.jetbrains.intellij.pycharm" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="pycharmPC" />
-          <option name="groupId" value="com.jetbrains" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="clion" />
-          <option name="groupId" value="com.jetbrains.intellij.clion" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="clion" />
-          <option name="groupId" value="com.jetbrains" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="riderRD" />
-          <option name="groupId" value="com.jetbrains.intellij.rider" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="riderRD" />
-          <option name="groupId" value="com.jetbrains" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="goland" />
-          <option name="groupId" value="com.jetbrains.intellij.goland" />
-        </LibraryCoordinatesState>
-        <LibraryCoordinatesState>
-          <option name="artifactId" value="goland" />
-          <option name="groupId" value="com.jetbrains" />
-        </LibraryCoordinatesState>
-      </list>
-    </option>
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index fe327906..223dc613 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,13 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="EntryPointsManager">
-    <list size="1">
-      <item index="0" class="java.lang.String" 
itemvalue="com.fasterxml.jackson.annotation.JsonValue" />
-    </list>
-  </component>
   <component name="ExternalStorageConfigurationManager" enabled="true" />
-  <component name="FrameworkDetectionExcludesConfiguration">
-    <file type="web" url="file://$PROJECT_DIR$" />
-  </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" 
default="true" project-jdk-name="11" project-jdk-type="JavaSDK" />
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" 
project-jdk-name="16" project-jdk-type="JavaSDK" />
 </project>
\ No newline at end of file
diff --git a/.idea/runConfigurations/SchedulingTest.xml 
b/.idea/runConfigurations/SchedulingTest.xml
deleted file mode 100644
index 6b78179c..00000000
--- a/.idea/runConfigurations/SchedulingTest.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="SchedulingTest" 
type="GradleRunConfiguration" factoryName="Gradle">
-    <ExternalSystemSettings>
-      <option name="executionName" />
-      <option name="externalProjectPath" value="$PROJECT_DIR$" />
-      <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value=":nexus:test --tests --quiet 
&quot;SchedulingTest&quot;" />
-      <option name="taskDescriptions">
-        <list />
-      </option>
-      <option name="taskNames">
-        <list />
-      </option>
-      <option name="vmOptions" />
-    </ExternalSystemSettings>
-    <ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
-    
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
-    <DebugAllEnabled>false</DebugAllEnabled>
-    <method v="2" />
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.idea/runConfigurations/run_sandbox.xml 
b/.idea/runConfigurations/run_sandbox.xml
deleted file mode 100644
index ca2e3410..00000000
--- a/.idea/runConfigurations/run_sandbox.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="run-sandbox" 
type="GradleRunConfiguration" factoryName="Gradle">
-    <ExternalSystemSettings>
-      <option name="executionName" />
-      <option name="externalProjectPath" value="$PROJECT_DIR$/sandbox" />
-      <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value="" />
-      <option name="taskDescriptions">
-        <list />
-      </option>
-      <option name="taskNames">
-        <list>
-          <option value="run" />
-        </list>
-      </option>
-      <option name="vmOptions" value="" />
-    </ExternalSystemSettings>
-    <GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
-    <method v="2" />
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.idea/runConfigurations/test_nexus.xml 
b/.idea/runConfigurations/test_nexus.xml
deleted file mode 100644
index 5b3bcf60..00000000
--- a/.idea/runConfigurations/test_nexus.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-    <configuration default="false" name="test-nexus" 
type="GradleRunConfiguration" factoryName="Gradle">
-        <ExternalSystemSettings>
-            <option name="executionName" />
-            <option name="externalProjectPath" value="$PROJECT_DIR$/nexus" />
-            <option name="externalSystemIdString" value="GRADLE" />
-            <option name="scriptParameters" value="" />
-            <option name="taskDescriptions">
-                <list />
-            </option>
-            <option name="taskNames">
-                <list>
-                    <option value="test" />
-                </list>
-            </option>
-            <option name="vmOptions" value="" />
-        </ExternalSystemSettings>
-        <GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
-        <method v="2" />
-    </configuration>
-</component>
\ No newline at end of file
diff --git a/.idea/runConfigurations/test_sandbox.xml 
b/.idea/runConfigurations/test_sandbox.xml
deleted file mode 100644
index 2bd23e79..00000000
--- a/.idea/runConfigurations/test_sandbox.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="test-sandbox" 
type="GradleRunConfiguration" factoryName="Gradle" 
show_console_on_std_out="true">
-    <ExternalSystemSettings>
-      <option name="executionName" />
-      <option name="externalProjectPath" value="$PROJECT_DIR$/sandbox" />
-      <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value="--info" />
-      <option name="taskDescriptions">
-        <list />
-      </option>
-      <option name="taskNames">
-        <list>
-          <option value="test" />
-        </list>
-      </option>
-      <option name="vmOptions" value="" />
-    </ExternalSystemSettings>
-    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
-    
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
-    <DebugAllEnabled>false</DebugAllEnabled>
-    <method v="2" />
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
deleted file mode 100644
index e96534fb..00000000
--- a/.idea/uiDesigner.xml
+++ /dev/null
@@ -1,124 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="Palette2">
-    <group name="Swing">
-      <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal 
Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" 
auto-create-binding="false" can-attach-label="false">
-        <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" 
fill="1" />
-      </item>
-      <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical 
Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" 
auto-create-binding="false" can-attach-label="false">
-        <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" 
fill="2" />
-      </item>
-      <item class="javax.swing.JPanel" 
icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" 
auto-create-binding="false" can-attach-label="false">
-        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" 
fill="3" />
-      </item>
-      <item class="javax.swing.JScrollPane" 
icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" 
auto-create-binding="false" can-attach-label="true">
-        <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" 
fill="3" />
-      </item>
-      <item class="javax.swing.JButton" 
icon="/com/intellij/uiDesigner/icons/button.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" 
fill="1" />
-        <initial-values>
-          <property name="text" value="Button" />
-        </initial-values>
-      </item>
-      <item class="javax.swing.JRadioButton" 
icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" 
fill="0" />
-        <initial-values>
-          <property name="text" value="RadioButton" />
-        </initial-values>
-      </item>
-      <item class="javax.swing.JCheckBox" 
icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" 
fill="0" />
-        <initial-values>
-          <property name="text" value="CheckBox" />
-        </initial-values>
-      </item>
-      <item class="javax.swing.JLabel" 
icon="/com/intellij/uiDesigner/icons/label.png" removable="false" 
auto-create-binding="false" can-attach-label="false">
-        <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" 
fill="0" />
-        <initial-values>
-          <property name="text" value="Label" />
-        </initial-values>
-      </item>
-      <item class="javax.swing.JTextField" 
icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" 
auto-create-binding="true" can-attach-label="true">
-        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" 
fill="1">
-          <preferred-size width="150" height="-1" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JPasswordField" 
icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" 
auto-create-binding="true" can-attach-label="true">
-        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" 
fill="1">
-          <preferred-size width="150" height="-1" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JFormattedTextField" 
icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" 
auto-create-binding="true" can-attach-label="true">
-        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" 
fill="1">
-          <preferred-size width="150" height="-1" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JTextArea" 
icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" 
auto-create-binding="true" can-attach-label="true">
-        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" 
fill="3">
-          <preferred-size width="150" height="50" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JTextPane" 
icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" 
auto-create-binding="true" can-attach-label="true">
-        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" 
fill="3">
-          <preferred-size width="150" height="50" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JEditorPane" 
icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" 
auto-create-binding="true" can-attach-label="true">
-        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" 
fill="3">
-          <preferred-size width="150" height="50" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JComboBox" 
icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" 
auto-create-binding="true" can-attach-label="true">
-        <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" 
fill="1" />
-      </item>
-      <item class="javax.swing.JTable" 
icon="/com/intellij/uiDesigner/icons/table.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" 
fill="3">
-          <preferred-size width="150" height="50" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JList" 
icon="/com/intellij/uiDesigner/icons/list.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" 
fill="3">
-          <preferred-size width="150" height="50" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JTree" 
icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" 
fill="3">
-          <preferred-size width="150" height="50" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JTabbedPane" 
icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" 
fill="3">
-          <preferred-size width="200" height="200" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JSplitPane" 
icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" 
auto-create-binding="false" can-attach-label="false">
-        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" 
fill="3">
-          <preferred-size width="200" height="200" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JSpinner" 
icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" 
auto-create-binding="true" can-attach-label="true">
-        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" 
fill="1" />
-      </item>
-      <item class="javax.swing.JSlider" 
icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" 
fill="1" />
-      </item>
-      <item class="javax.swing.JSeparator" 
icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" 
auto-create-binding="false" can-attach-label="false">
-        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" 
fill="3" />
-      </item>
-      <item class="javax.swing.JProgressBar" 
icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" 
fill="1" />
-      </item>
-      <item class="javax.swing.JToolBar" 
icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" 
auto-create-binding="false" can-attach-label="false">
-        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" 
fill="1">
-          <preferred-size width="-1" height="20" />
-        </default-constraints>
-      </item>
-      <item class="javax.swing.JToolBar$Separator" 
icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" 
auto-create-binding="false" can-attach-label="false">
-        <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" 
fill="1" />
-      </item>
-      <item class="javax.swing.JScrollBar" 
icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" 
auto-create-binding="true" can-attach-label="false">
-        <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" 
fill="2" />
-      </item>
-    </group>
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 00000000..12738e16
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="AutoImportSettings">
+    <option name="autoReloadType" value="SELECTIVE" />
+  </component>
+  <component name="ChangeListManager">
+    <list default="true" id="9436eb1e-de48-4f11-8ff7-f359340cb458" 
name="Changes" comment="">
+      <change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
+      <change 
afterPath="$PROJECT_DIR$/bank/src/test/kotlin/LibeuFinApiTest.kt" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/$PRODUCT_WORKSPACE_FILE$" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/codeStyles/Project.xml" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/codeStyles/codeStyleConfig.xml" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/dictionaries/dold.xml" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/gradle.xml" beforeDir="false" 
afterPath="$PROJECT_DIR$/.idea/gradle.xml" afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/kotlinc.xml" beforeDir="false" 
afterPath="$PROJECT_DIR$/.idea/kotlinc.xml" afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/.idea/libraries-with-intellij-classes.xml" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" 
afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/.idea/runConfigurations/SchedulingTest.xml" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/.idea/runConfigurations/run_sandbox.xml" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/.idea/runConfigurations/test_nexus.xml" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/.idea/runConfigurations/test_sandbox.xml" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/uiDesigner.xml" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/bank/README" beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/README" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/bank/build.gradle" beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/build.gradle" afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/CircuitApi.kt"
 beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/CircuitApi.kt" 
afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/ConversionService.kt"
 beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/ConversionService.kt"
 afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/DB.kt" 
beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/DB.kt" 
afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Database.kt" 
beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Database.kt" 
afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/EbicsProtocolBackend.kt"
 beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/EbicsProtocolBackend.kt"
 afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt" 
beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt" 
afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/JSON.kt" 
beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/JSON.kt" 
afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Main.kt" 
beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Main.kt" 
afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/XMLEbicsConverter.kt"
 beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/XMLEbicsConverter.kt"
 afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/bankAccount.kt"
 beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/bankAccount.kt"
 afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/bank/src/main/resources/logback.xml" 
beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/resources/logback.xml" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/BalanceTest.kt" 
beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/test/kotlin/BalanceTest.kt" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/DBTest.kt" 
beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/test/kotlin/DBTest.kt" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/DatabaseTest.kt" 
beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/test/kotlin/DatabaseTest.kt" afterDir="false" 
/>
+      <change 
beforePath="$PROJECT_DIR$/bank/src/test/kotlin/EbicsErrorTest.kt" 
beforeDir="false" 
afterPath="$PROJECT_DIR$/bank/src/test/kotlin/EbicsErrorTest.kt" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/StringsTest.kt" 
beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/test/kotlin/StringsTest.kt" 
afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/database-versioning/new/libeufin-bank-0001.sql" 
beforeDir="false" 
afterPath="$PROJECT_DIR$/database-versioning/new/libeufin-bank-0001.sql" 
afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/database-versioning/new/procedures.sql" 
beforeDir="false" 
afterPath="$PROJECT_DIR$/database-versioning/new/procedures.sql" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/build.gradle" beforeDir="false" 
afterPath="$PROJECT_DIR$/nexus/build.gradle" afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/ConversionServiceTest.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/DbEventTest.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/EbicsTest.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/Iso20022Test.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/JsonTest.kt" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/LetterFormatTest.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/MakeEnv.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/NexusApiTest.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/PainTest.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/PostFinance.kt" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SandboxAccessApiTest.kt" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SandboxBankAccountTest.kt" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SandboxCircuitApiTest.kt" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SandboxLegacyApiTest.kt" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SchedulingTest.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SplitString.kt" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SubjectNormalization.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/TalerTest.kt" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/XLibeufinBankTest.kt" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/XPathTest.kt" 
beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml"
 beforeDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/nexus/src/test/resources/logback-test.xml" 
beforeDir="false" />
+      <change beforePath="$PROJECT_DIR$/util/build.gradle" beforeDir="false" 
afterPath="$PROJECT_DIR$/util/build.gradle" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/Config.kt" 
beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/Config.kt" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/DB.kt" 
beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/DB.kt" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/HTTP.kt" 
beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/HTTP.kt" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/iban.kt" 
beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/iban.kt" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/startServer.kt" 
beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/startServer.kt" 
afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/time.kt" 
beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/time.kt" 
afterDir="false" />
+      <change 
beforePath="$PROJECT_DIR$/util/src/test/kotlin/StartServerTest.kt" 
beforeDir="false" />
+    </list>
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="ExternalProjectsData">
+    <projectState path="$PROJECT_DIR$">
+      <ProjectState />
+    </projectState>
+  </component>
+  <component name="ExternalProjectsManager">
+    <system id="GRADLE">
+      <state>
+        <task path="$PROJECT_DIR$">
+          <activation />
+        </task>
+        <projects_view />
+      </state>
+    </system>
+  </component>
+  <component name="Git.Settings">
+    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
+  </component>
+  <component name="MarkdownSettingsMigration">
+    <option name="stateVersion" value="1" />
+  </component>
+  <component name="ProjectId" id="2V4jS1FHAIvLu5eYODKLzGxNSP4" />
+  <component name="ProjectViewState">
+    <option name="hideEmptyMiddlePackages" value="true" />
+    <option name="showLibraryContents" value="true" />
+  </component>
+  <component name="PropertiesComponent">{
+  &quot;keyToString&quot;: {
+    &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
+    &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;
+  }
+}</component>
+  <component name="RunManager" 
selected="Gradle.LibeuFinApiTest.createAccountTest">
+    <configuration name="DatabaseTest" type="GradleRunConfiguration" 
factoryName="Gradle" temporary="true">
+      <ExternalSystemSettings>
+        <option name="executionName" />
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="externalSystemIdString" value="GRADLE" />
+        <option name="scriptParameters" value="--quiet" />
+        <option name="taskDescriptions">
+          <list />
+        </option>
+        <option name="taskNames">
+          <list>
+            <option value=":bank:test" />
+            <option value="--tests" />
+            <option value="&quot;DatabaseTest&quot;" />
+          </list>
+        </option>
+        <option name="vmOptions" />
+      </ExternalSystemSettings>
+      
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
+      
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+      <DebugAllEnabled>false</DebugAllEnabled>
+      <method v="2" />
+    </configuration>
+    <configuration name="DatabaseTest.bearerTokenTest" 
type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
+      <ExternalSystemSettings>
+        <option name="executionName" />
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="externalSystemIdString" value="GRADLE" />
+        <option name="scriptParameters" value="--quiet" />
+        <option name="taskDescriptions">
+          <list />
+        </option>
+        <option name="taskNames">
+          <list>
+            <option value=":bank:test" />
+            <option value="--tests" />
+            <option value="&quot;DatabaseTest.bearerTokenTest&quot;" />
+          </list>
+        </option>
+        <option name="vmOptions" />
+      </ExternalSystemSettings>
+      
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
+      
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+      <DebugAllEnabled>false</DebugAllEnabled>
+      <method v="2" />
+    </configuration>
+    <configuration name="LibeuFinApiTest.createAccountTest" 
type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
+      <ExternalSystemSettings>
+        <option name="executionName" />
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="externalSystemIdString" value="GRADLE" />
+        <option name="scriptParameters" value="--quiet" />
+        <option name="taskDescriptions">
+          <list />
+        </option>
+        <option name="taskNames">
+          <list>
+            <option value=":bank:test" />
+            <option value="--tests" />
+            <option value="&quot;LibeuFinApiTest.createAccountTest&quot;" />
+          </list>
+        </option>
+        <option name="vmOptions" />
+      </ExternalSystemSettings>
+      
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
+      
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+      <DebugAllEnabled>false</DebugAllEnabled>
+      <method v="2" />
+    </configuration>
+    <configuration name="libeufin [dependencies]" 
type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
+      <ExternalSystemSettings>
+        <option name="executionName" />
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="externalSystemIdString" value="GRADLE" />
+        <option name="scriptParameters" />
+        <option name="taskDescriptions">
+          <list />
+        </option>
+        <option name="taskNames">
+          <list>
+            <option value="dependencies" />
+          </list>
+        </option>
+        <option name="vmOptions" />
+      </ExternalSystemSettings>
+      <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+      
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+      <DebugAllEnabled>false</DebugAllEnabled>
+      <method v="2" />
+    </configuration>
+    <list>
+      <item itemvalue="Gradle.libeufin [dependencies]" />
+      <item itemvalue="Gradle.DatabaseTest" />
+      <item itemvalue="Gradle.DatabaseTest.bearerTokenTest" />
+      <item itemvalue="Gradle.LibeuFinApiTest.createAccountTest" />
+    </list>
+    <recent_temporary>
+      <list>
+        <item itemvalue="Gradle.LibeuFinApiTest.createAccountTest" />
+        <item itemvalue="Gradle.libeufin [dependencies]" />
+        <item itemvalue="Gradle.DatabaseTest" />
+        <item itemvalue="Gradle.DatabaseTest.bearerTokenTest" />
+      </list>
+    </recent_temporary>
+  </component>
+  <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" 
CustomDictionaries="0" DefaultDictionary="application-level" 
UseSingleDictionary="true" transferred="true" />
+  <component name="TaskManager">
+    <task active="true" id="Default" summary="Default task">
+      <changelist id="9436eb1e-de48-4f11-8ff7-f359340cb458" name="Changes" 
comment="" />
+      <created>1694102242996</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1694102242996</updated>
+    </task>
+    <servers />
+  </component>
+</project>
\ No newline at end of file
diff --git a/bank/build.gradle b/bank/build.gradle
index 71e44a5a..a7925da7 100644
--- a/bank/build.gradle
+++ b/bank/build.gradle
@@ -73,6 +73,9 @@ dependencies {
     implementation "io.ktor:ktor-server-test-host:$ktor_version"
     implementation "io.ktor:ktor-auth:$ktor_auth_version"
     implementation "io.ktor:ktor-serialization-jackson:$ktor_version"
+    // implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
+    // implementation("io.ktor:ktor-serialization-gson:$ktor_version")
+    implementation "io.ktor:ktor-server-request-validation:$ktor_version"
 
     testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21'
     testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21'
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CircuitApi.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/CircuitApi.kt
deleted file mode 100644
index 93fcdb6a..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/CircuitApi.kt
+++ /dev/null
@@ -1,841 +0,0 @@
-package tech.libeufin.bank
-
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.server.application.*
-import io.ktor.http.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.bank.CashoutOperationsTable.uuid
-import tech.libeufin.util.*
-import java.io.File
-import java.io.InputStreamReader
-import java.math.BigDecimal
-import java.util.concurrent.TimeUnit
-import kotlin.text.toByteArray
-
-// CIRCUIT API TYPES
-/**
- * This type is used by clients to ask the bank a cash-out
- * estimate to show to the customer before they confirm the
- * cash-out creation.
- */
-data class CircuitCashoutEstimateRequest(
-    /**
-     * This is the amount that the customer will get deducted
-     * from their regio bank account to fuel the cash-out operation.
-     */
-    val amount_debit: String
-)
-data class CircuitCashoutRequest(
-    val subject: String?,
-    val amount_debit: String, // As specified by the user via the SPA.
-    val amount_credit: String, // What actually to transfer after the rates.
-    /**
-     * The String type here allows more flexibility with regard to
-     * the supported TAN methods.  This way, supported TAN methods
-     * can be specified via the configuration or when starting the
-     * bank.  OTOH, catching unsupported TAN methods only via the
-     * 'enum' type would require to change the source code upon every
-     * change in the TAN policy.
-     */
-    val tan_channel: String?
-)
-const val FIAT_CURRENCY = "CHF" // FIXME: make configurable.
-// Configuration response:
-data class ConfigResp(
-    val name: String = "circuit",
-    val version: String = PROTOCOL_VERSION_UNIFIED,
-    val ratios_and_fees: RatioAndFees,
-    val fiat_currency: String = FIAT_CURRENCY
-)
-
-// After fixing #7527, the values held by this
-// type must be read from the configuration.
-data class RatioAndFees(
-    val buy_at_ratio: Float = 1F,
-    val sell_at_ratio: Float = 0.95F,
-    val buy_in_fee: Float = 0F,
-    val sell_out_fee: Float = 0F
-)
-val ratiosAndFees = RatioAndFees()
-
-// User registration request
-data class CircuitAccountRequest(
-    val username: String,
-    val password: String,
-    val contact_data: CircuitContactData,
-    val name: String,
-    val cashout_address: String, // payto
-    val internal_iban: String? // Shall be "= null" ?
-)
-// User contact data to send the TAN.
-data class CircuitContactData(
-    val email: String?,
-    val phone: String?
-)
-
-data class CircuitAccountReconfiguration(
-    val contact_data: CircuitContactData,
-    val cashout_address: String?,
-    val name: String? = null
-)
-
-data class AccountPasswordChange(
-    val new_password: String
-)
-
-/**
- * That doesn't belong to the Access API because it
- * contains the cash-out address and the contact data.
- */
-data class CircuitAccountInfo(
-    val username: String,
-    val iban: String,
-    val contact_data: CircuitContactData,
-    val name: String,
-    val cashout_address: String?
-)
-
-data class CashoutOperationInfo(
-    val status: CashoutOperationStatus,
-    val amount_credit: String,
-    val amount_debit: String,
-    val subject: String,
-    val creation_time: Long, // milliseconds
-    val confirmation_time: Long?, // milliseconds
-    val tan_channel: SupportedTanChannels,
-    val account: String,
-    val cashout_address: String,
-    val ratios_and_fees: RatioAndFees
-)
-
-data class CashoutConfirmation(val tan: String)
-
-// Validate phone number
-fun checkPhoneNumber(phoneNumber: String): Boolean {
-    // From Taler TypeScript
-    // /^\+[0-9 ]*$/;
-    val regex = "^\\+[1-9][0-9]+$"
-    val R = Regex(regex)
-    return R.matches(phoneNumber)
-}
-
-// Validate e-mail address
-fun checkEmailAddress(emailAddress: String): Boolean {
-    // From Taler TypeScript:
-    // 
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-    val regex = "^[a-zA-Z0-9\\.]+@[a-zA-Z0-9\\.]+$"
-    val R = Regex(regex)
-    return R.matches(emailAddress)
-}
-
-fun throwIfInstitutionalName(resourceName: String) {
-    if (resourceName == "bank" || resourceName == "admin")
-        throw forbidden("Can't operate on institutional resource 
'$resourceName'")
-}
-
-fun generateCashoutSubject(
-    amountCredit: AmountWithCurrency,
-    amountDebit: AmountWithCurrency
-): String {
-    return "Cash-out of ${amountDebit.currency}:${amountDebit.amount}" +
-            " to ${amountCredit.currency}:${amountCredit.amount}"
-}
-
-/**
- * By default, it takes the amount in the regional currency
- * and applies ratio and fees to convert it to fiat.  If the
- * 'fromCredit' parameter is true, then it does the inverse
- * operation: returns the regional amount that would lead to
- * such fiat amount given in the 'amount' parameter.
- */
-fun applyCashoutRatioAndFee(
-    amount: BigDecimal,
-    ratiosAndFees: RatioAndFees,
-    fromCredit: Boolean = false
-): BigDecimal {
-    // Normal case, when the calculation starts from the regional amount.
-    if (!fromCredit) {
-        val maybeCashoutAmount =  ((amount * 
ratiosAndFees.sell_at_ratio.toBigDecimal()) -
-                ratiosAndFees.sell_out_fee.toBigDecimal()).roundToTwoDigits()
-        // throws 500, since bank should not allow to get negative fiat 
amounts.
-        if (maybeCashoutAmount < BigDecimal.ZERO) {
-            logger.error("Cash-out operation caused a negative fiat output." +
-                    "  Regional amount was '$amount', cash-out ratio is 
'${ratiosAndFees.sell_at_ratio}," +
-                    " cash-out fee is '${ratiosAndFees.sell_out_fee}''"
-            )
-            throw internalServerError("Applying cash-out fees yielded negative 
fiat amount.")
-        }
-        return maybeCashoutAmount
-    }
-    // UI convenient case, when the calculation starts from the
-    // desired fiat amount that the user wants eventually be paid.
-    return ((amount + ratiosAndFees.sell_out_fee.toBigDecimal()) /
-            ratiosAndFees.sell_at_ratio.toBigDecimal()).roundToTwoDigits()
-}
-
-/**
- * NOTE: future versions take the supported TAN method from
- * the configuration, or options passed when starting the bank.
- */
-const val LIBEUFIN_TAN_TMP_FILE = "/tmp/libeufin-cashout-tan.txt"
-enum class SupportedTanChannels {
-    SMS,
-    EMAIL,
-    FILE // Test channel writing the TAN to the LIBEUFIN_TAN_TMP_FILE location.
-}
-fun isTanChannelSupported(tanChannel: String): Boolean {
-    enumValues<SupportedTanChannels>().forEach {
-        if (tanChannel.uppercase() == it.name) return true
-    }
-    return false
-}
-
-var EMAIL_TAN_CMD: String? = null
-var SMS_TAN_CMD: String? = null
-
-// Convenience class to collect TAN data.
-private data class TanData(
-    val cmd: String,
-    val address: String,
-    val msg: String
-)
-
-/**
- * Runs the command and returns True/False if that succeeded/failed.
- * A failed command causes "500 Internal Server Error" to be responded
- * along a cash-out creation.  'address' is a phone number or a e-mail address,
- * according to which TAN channel is used.  'message' carries the TAN.
- *
- * The caller is expected to manage the exceptions thrown by this function.
- */
-fun runTanCommand(command: String, address: String, message: String): Boolean {
-    val prep = ProcessBuilder(command, address)
-    prep.redirectErrorStream(true) // merge STDOUT and STDERR
-    val proc = prep.start()
-    proc.outputStream.write(message.toByteArray())
-    proc.outputStream.flush(); proc.outputStream.close()
-    var isSuccessful = false
-    // Wait the command to finish.
-    proc.waitFor(10L, TimeUnit.SECONDS)
-    // Check if timed out.  Kill if so.
-    if (proc.isAlive) {
-        logger.error("TAN command '$command' timed out, killing it.")
-        proc.destroy()
-        // Check if exited gracefully.  Kill forcibly if not.
-        proc.waitFor(5L, TimeUnit.SECONDS)
-        if (proc.isAlive) {
-            logger.error("TAN command '$command' didn't terminate after 
killing it.  Try forcefully.")
-            proc.destroyForcibly()
-        }
-    }
-    // Check if successful.  Switch the state if so.
-    if (proc.exitValue() == 0) isSuccessful = true
-    // Log STDOUT and STDERR if failed.
-    if (!isSuccessful)
-        logger.error(InputStreamReader(proc.inputStream).readText())
-    return isSuccessful
-}
-
-fun circuitApi(circuitRoute: Route) {
-    // Abort a cash-out operation.
-    circuitRoute.post("/cashouts/{uuid}/abort") {
-        call.request.basicAuth() // both admin and author allowed
-        val arg = call.expectUriComponent("uuid")
-        // Parse and check the UUID.
-        val maybeUuid = parseUuid(arg)
-        val maybeOperation = transaction {
-            CashoutOperationEntity.find { uuid eq maybeUuid }.firstOrNull()
-        }
-        if (maybeOperation == null)
-            throw notFound("Cash-out operation $uuid not found.")
-        if (maybeOperation.status == CashoutOperationStatus.CONFIRMED)
-            throw SandboxError(
-                HttpStatusCode.PreconditionFailed,
-                "Cash-out operation '$uuid' was confirmed already."
-            )
-        if (maybeOperation.status != CashoutOperationStatus.PENDING)
-            throw internalServerError("Found an unsupported cash-out operation 
state: ${maybeOperation.status}")
-        // Operation found and pending: delete from the database.
-        transaction { maybeOperation.delete() }
-        call.respond(HttpStatusCode.NoContent)
-        return@post
-    }
-    // Confirm a cash-out operation
-    circuitRoute.post("/cashouts/{uuid}/confirm") {
-        val user = call.request.basicAuth()
-        // Exclude admin from this operation.
-        if (user == "admin" || user == "bank")
-            throw conflict("Institutional user '$user' shouldn't confirm any 
cash-out.")
-        // Get the operation identifier.
-        val operationUuid = parseUuid(call.expectUriComponent("uuid"))
-        val op = transaction {
-            CashoutOperationEntity.find {
-                uuid eq operationUuid
-            }.firstOrNull()
-        }
-        // 404 if the operation is not found.
-        if (op == null)
-            throw notFound("Cash-out operation $operationUuid not found")
-        /**
-         * Check the TAN.  Give precedence to the TAN found
-         * in the environment, for testing purposes.  If that's
-         * not found, then check with the actual TAN found in
-         * the database.
-         */
-        val req = call.receive<CashoutConfirmation>()
-        val maybeTanFromEnv = System.getenv("LIBEUFIN_CASHOUT_TEST_TAN")
-        if (maybeTanFromEnv != null)
-            logger.warn("TAN being read from the environment.  Assuming tests 
are being run")
-        val checkTan = maybeTanFromEnv ?: op.tan
-        if (req.tan != checkTan)
-            throw forbidden("The confirmation of '${op.uuid}' has a wrong TAN 
'${req.tan}'")
-        /**
-         * Correct TAN.  Wire the funds to the admin's bank account.  After
-         * this step, the conversion monitor should detect this payment and
-         * soon initiate the final transfer towards the user fiat bank account.
-         * NOTE: the funds availability got already checked when this operation
-         * was created.  On top of that, the 'wireTransfer()' helper does also
-         * check for funds availability.  */
-        val customer = maybeGetCustomer(user ?: throw SandboxError(
-            HttpStatusCode.ServiceUnavailable,
-            "This endpoint isn't served when the authentication is disabled."
-        ))
-        transaction {
-            if (op.cashoutAddress != customer?.cashout_address) throw conflict(
-                "Inconsistent cash-out address: ${op.cashoutAddress} vs 
${customer?.cashout_address}"
-            )
-            // 412 if the operation got already confirmed.
-            if (op.status == CashoutOperationStatus.CONFIRMED)
-                throw SandboxError(
-                    HttpStatusCode.PreconditionFailed,
-                    "Cash-out operation $operationUuid was already confirmed."
-                )
-            wireTransfer(
-                debitAccount = op.account,
-                creditAccount = "admin",
-                subject = op.subject,
-                amount = op.amountDebit
-            )
-            op.status = CashoutOperationStatus.CONFIRMED
-            op.confirmationTime = getSystemTimeNow().toInstant().toEpochMilli()
-            // TODO(signal this payment over LIBEUFIN_REGIO_INCOMING)
-        }
-        call.respond(HttpStatusCode.NoContent)
-        return@post
-    }
-    // Retrieve the status of a cash-out operation.
-    circuitRoute.get("/cashouts/{uuid}") {
-        call.request.basicAuth() // both admin and author
-        val operationUuid = call.expectUriComponent("uuid")
-        // Parse and check the UUID.
-        val maybeUuid = parseUuid(operationUuid)
-        // Get the operation from the database.
-        val maybeOperation = transaction {
-            CashoutOperationEntity.find { uuid eq maybeUuid }.firstOrNull()
-        }
-        if (maybeOperation == null)
-            throw notFound("Cash-out operation $operationUuid not found.")
-        val ret = CashoutOperationInfo(
-            amount_credit = maybeOperation.amountCredit,
-            amount_debit = maybeOperation.amountDebit,
-            subject = maybeOperation.subject,
-            status = maybeOperation.status,
-            creation_time = maybeOperation.creationTime,
-            confirmation_time = maybeOperation.confirmationTime,
-            tan_channel = maybeOperation.tanChannel,
-            account = maybeOperation.account,
-            cashout_address = maybeOperation.cashoutAddress,
-            ratios_and_fees = RatioAndFees(
-                buy_in_fee = maybeOperation.buyInFee.toFloat(),
-                buy_at_ratio = maybeOperation.buyAtRatio.toFloat(),
-                sell_out_fee = maybeOperation.sellOutFee.toFloat(),
-                sell_at_ratio = maybeOperation.sellAtRatio.toFloat()
-            )
-        )
-        call.respond(ret)
-        return@get
-    }
-    // Gets the list of all the cash-out operations,
-    // or those belonging to the account given as a parameter.
-    circuitRoute.get("/cashouts") {
-        val user = call.request.basicAuth()
-        val whichAccount = call.request.queryParameters["account"]
-        /**
-         * Only admin's allowed to omit the target account (= get
-         * all the accounts) or to check other customers cash-out
-         * operations.
-         */
-        if (user != "admin" && whichAccount != user) throw forbidden(
-            "Ordinary users can only request their own account"
-        )
-        /**
-         * At this point, the client has the rights over the account(s)
-         * whose operations are to be returned.  Double-checking that
-         * Admin doesn't ask its own cash-outs, since that's not supported.
-         */
-        if (whichAccount == "admin") throw badRequest("Cash-out for admin is 
not supported")
-
-        // Preparing the response.
-        val node = jacksonObjectMapper().createObjectNode()
-        val maybeArray = node.putArray("cashouts")
-
-        if (whichAccount == null) { // no target account, return all the 
cash-outs
-            transaction {
-                CashoutOperationEntity.all().forEach {
-                    maybeArray.add(it.uuid.toString())
-                }
-            }
-        } else { // do filter on the target account.
-            transaction {
-                CashoutOperationEntity.find {
-                    CashoutOperationsTable.account eq whichAccount
-                }.forEach {
-                    maybeArray.add(it.uuid.toString())
-                }
-            }
-        }
-        if (maybeArray.size() == 0) {
-            call.respond(HttpStatusCode.NoContent)
-            return@get
-        }
-        call.respond(node)
-        return@get
-    }
-    circuitRoute.get("/cashouts/estimates") {
-        call.request.basicAuth()
-        val demobank = ensureDemobank(call)
-        // Optionally parsing param 'amount_debit' into number and checking 
its currency
-        val maybeAmountDebit: String? = 
call.request.queryParameters["amount_debit"]
-        val amountDebit: BigDecimal? = if (maybeAmountDebit != null) {
-            val amount = parseAmount(maybeAmountDebit)
-            if (amount.currency != demobank.config.currency) throw badRequest(
-                "parameter 'amount_debit' has the wrong currency: 
${amount.currency}"
-            )
-            try { amount.amount.toBigDecimal() } catch (e: Exception) {
-                throw badRequest("Cannot extract a number from 'amount_debit'")
-            }
-        } else null
-        // Optionally parsing param 'amount_credit' into number and checking 
its currency
-        val maybeAmountCredit: String? = 
call.request.queryParameters["amount_credit"]
-        val amountCredit: BigDecimal? = if (maybeAmountCredit != null) {
-            val amount = parseAmount(maybeAmountCredit)
-            if (amount.currency != FIAT_CURRENCY) throw badRequest(
-                "parameter 'amount_credit' has the wrong currency: 
${amount.currency}"
-            )
-            try { amount.amount.toBigDecimal() } catch (e: Exception) {
-                throw badRequest("Cannot extract a number from 
'amount_credit'")
-            }
-        } else null
-        val respAmountCredit = if (amountDebit != null) {
-            val estimate = applyCashoutRatioAndFee(amountDebit, ratiosAndFees)
-            if (amountCredit != null && estimate != amountCredit) throw 
badRequest(
-                "Wrong calculation found in 'amount_credit', bank estimates: 
$estimate"
-            )
-            estimate
-        } else null
-        if (amountDebit == null && amountCredit == null) throw badRequest(
-            "Both 'amount_credit' and 'amount_debit' are missing"
-        )
-        val respAmountDebit = if (amountCredit != null) {
-            val estimate = applyCashoutRatioAndFee(
-                amountCredit,
-                ratiosAndFees,
-                fromCredit = true
-            )
-            if (amountDebit != null && estimate != amountDebit) throw 
badRequest(
-                "Wrong calculation found in 'amount_credit', bank estimates: 
$estimate"
-            )
-            estimate
-        } else null
-        call.respond(object {
-            val amount_credit = "$FIAT_CURRENCY:$respAmountCredit"
-            val amount_debit = "${demobank.config.currency}:$respAmountDebit"
-        })
-        return@get
-    }
-
-    // Create a cash-out operation.
-    circuitRoute.post("/cashouts") {
-        val user = call.request.basicAuth()
-        if (user == "admin" || user == "bank") throw forbidden("$user can't 
cash-out.")
-        // No suitable default user, when the authentication is disabled.
-        if (user == null) throw SandboxError(
-            HttpStatusCode.ServiceUnavailable,
-            "This endpoint isn't served when the authentication is disabled."
-        )
-        val req = call.receive<CircuitCashoutRequest>()
-
-        // validate amounts: well-formed and supported currency.
-        val amountDebit = parseAmount(req.amount_debit) // amount before rates.
-        val amountCredit = parseAmount(req.amount_credit) // amount after 
rates, as expected by the client
-        val demobank = ensureDemobank(call)
-        // Currency check of the cash-out's circuit part.
-        if (amountDebit.currency != demobank.config.currency)
-            throw badRequest("'${req::amount_debit.name}' 
(${req.amount_debit})" +
-                    " doesn't match the regional currency 
(${demobank.config.currency})"
-            )
-        // Currency check of the cash-out's fiat part.
-        if (amountCredit.currency != FIAT_CURRENCY)
-            throw badRequest("'${req::amount_credit.name}' 
(${req.amount_credit})" +
-                    " doesn't match the fiat currency ($FIAT_CURRENCY)."
-            )
-        // check if TAN is supported.  Default to SMS, if that's missing.
-        val tanChannel = req.tan_channel?.uppercase() ?: 
SupportedTanChannels.SMS.name
-        if (!isTanChannelSupported(tanChannel))
-            throw SandboxError(
-                HttpStatusCode.ServiceUnavailable,
-                "TAN channel '$tanChannel' not supported."
-            )
-        // check if the user contact data would allow the TAN channel.
-        val customer: DemobankCustomerEntity? = maybeGetCustomer(username = 
user)
-        if (customer == null) throw internalServerError(
-            "Customer profile '$user' not found after authenticating it."
-        )
-        if (customer.cashout_address == null) throw SandboxError(
-            HttpStatusCode.PreconditionFailed,
-            "Cash-out address not found.  Did the user register via Circuit 
API?"
-        )
-        if ((tanChannel == SupportedTanChannels.EMAIL.name) && (customer.email 
== null))
-            throw conflict("E-mail address not found for '$user'.  Can't send 
the TAN")
-        if ((tanChannel == SupportedTanChannels.SMS.name) && (customer.phone 
== null))
-            throw conflict("Phone number not found for '$user'.  Can't send 
the TAN")
-        // check rates correctness
-        val amountDebitAsNumber = BigDecimal(amountDebit.amount)
-        val expectedAmountCredit = 
applyCashoutRatioAndFee(amountDebitAsNumber, ratiosAndFees)
-        val amountCreditAsNumber = 
BigDecimal(amountCredit.amount).roundToTwoDigits()
-        if (expectedAmountCredit != amountCreditAsNumber) {
-            throw badRequest("Rates application are incorrect." +
-                    "  The expected amount to credit is: 
${expectedAmountCredit}," +
-                    " but ${amountCredit.amount} was specified.")
-        }
-        // check that the balance is sufficient
-        val balance = getBalance(
-            user,
-            demobank.name
-        )
-        val balanceCheck = balance - amountDebitAsNumber
-        if (balanceCheck < BigDecimal.ZERO && balanceCheck.abs() > 
BigDecimal(demobank.config.usersDebtLimit))
-            throw SandboxError(
-                HttpStatusCode.PreconditionFailed,
-                "Cash-out not possible due to insufficient funds.  Balance 
${balance.toPlainString()} would reach ${balanceCheck.toPlainString()}"
-            )
-        // generate a subject if that's missing
-        val cashoutSubject = req.subject ?: generateCashoutSubject(
-            amountCredit = amountCredit,
-            amountDebit = amountDebit
-        )
-        val op = transaction {
-            CashoutOperationEntity.new {
-                this.amountDebit = req.amount_debit
-                this.amountCredit = req.amount_credit
-                this.buyAtRatio = ratiosAndFees.buy_at_ratio.toString()
-                this.buyInFee = ratiosAndFees.buy_in_fee.toString()
-                this.sellAtRatio = ratiosAndFees.sell_at_ratio.toString()
-                this.sellOutFee = ratiosAndFees.sell_out_fee.toString()
-                this.subject = cashoutSubject
-                this.creationTime = 
getSystemTimeNow().toInstant().toEpochMilli()
-                this.tanChannel = SupportedTanChannels.valueOf(tanChannel)
-                this.account = user
-                this.tan = getRandomString(5)
-                this.cashoutAddress = customer.cashout_address ?: throw 
internalServerError(
-                    "Cash-out address for '$user' not found, after previous 
check succeeded"
-                )
-            }
-        }
-        when (tanChannel) {
-            SupportedTanChannels.EMAIL.name -> {
-                val isSuccessful = try {
-                    runTanCommand(
-                        command = EMAIL_TAN_CMD ?: throw internalServerError(
-                            "E-mail TAN supported but the command" +
-                                    " was not found.  See the --email-tan 
option from 'serve'"
-                        ),
-                        address = customer.email ?: throw internalServerError(
-                            "Customer has no e-mail address, but previous 
check should" +
-                                    " have detected it!"
-                        ),
-                        message = op.tan
-                    )
-                } catch (e: Exception) {
-                    logger.error("Sending the e-mail TAN to ${customer.email} 
was impossible." +
-                            "  Reason: ${e.message}")
-                    throw internalServerError("Could not send the e-mail TAN.")
-                }
-                if (!isSuccessful)
-                    throw internalServerError("E-mail TAN command failed.")
-            }
-            SupportedTanChannels.SMS.name -> {
-                val isSuccessful = try {
-                    runTanCommand(
-                        command = SMS_TAN_CMD ?: throw internalServerError(
-                            "SMS TAN supported but the command" +
-                                    " was not found.  See the --sms-tan option 
from 'serve'"
-                        ),
-                        address = customer.phone ?: throw internalServerError(
-                        "Customer has no phone number, but previous check 
should" +
-                                " have detected it!"
-
-                        ),
-                        message = op.tan
-                    )
-
-                } catch (e: Exception) {
-                    logger.error("Sending the SMS TAN to ${customer.phone} was 
impossible." +
-                            "  Reason: ${e.message}")
-                    throw internalServerError("Could not send the SMS TAN.")
-                }
-                if (!isSuccessful)
-                    throw internalServerError("SMS TAN command failed.")
-            }
-            SupportedTanChannels.FILE.name -> {
-                try {
-                    File(LIBEUFIN_TAN_TMP_FILE).writeText(op.tan)
-                } catch (e: Exception) {
-                    logger.error("Could not write to $LIBEUFIN_TAN_TMP_FILE.  
Reason: ${e.message}")
-                    throw internalServerError("File TAN failed.")
-                }
-            }
-            else ->
-                throw internalServerError("The bank tried an unsupported TAN 
channel: $tanChannel.")
-        }
-        call.respond(HttpStatusCode.Accepted, object {val uuid = op.uuid})
-        return@post
-    }
-    // Get Circuit-relevant account data.
-    circuitRoute.get("/accounts/{resourceName}") {
-        val username = call.request.basicAuth()
-        val resourceName = call.expectUriComponent("resourceName")
-        throwIfInstitutionalName(resourceName)
-        if (!allowOwnerOrAdmin(username, resourceName)) throw forbidden(
-            "User $username has no rights over $resourceName"
-        )
-        val customer = getCustomer(resourceName)
-        /**
-         * CUSTOMER AND BANK ACCOUNT INVARIANT.
-         *
-         * After having found a 'customer' associated with the resourceName
-         * - see previous line -, the bank must ensure that a 'bank account'
-         * exist under the same resourceName.  If that fails, the bank broke 
the
-         * invariant and should respond 500.
-         */
-        val bankAccount = getBankAccountFromLabel(resourceName, withBankFault 
= true)
-        /**
-         * Throwing when name or cash-out address aren't found ensures
-         * that the customer was indeed added via the Circuit API, as opposed
-         * to the Access API.
-         */
-        call.respond(CircuitAccountInfo(
-            username = customer.username,
-            name = customer.name ?: throw internalServerError(
-                "Account '$resourceName' was found without owner's name."
-            ),
-            cashout_address = customer.cashout_address,
-            contact_data = CircuitContactData(
-                email = customer.email,
-                phone = customer.phone
-            ),
-            iban = bankAccount.iban
-        ))
-        return@get
-    }
-
-    // Get summary of all the accounts.
-    circuitRoute.get("/accounts") {
-        call.request.basicAuth(onlyAdmin = true)
-        val maybeFilter: String? = call.request.queryParameters["filter"]
-        /**
-         * Equip the given filter with left and right catch-all wildcards,
-         * otherwise use one catch-all wildcard.
-         */
-        val filter = if (maybeFilter != null) {
-            "%${maybeFilter}%"
-        } else "%"
-        val customers = mutableListOf<Any>()
-        val demobank = ensureDemobank(call)
-        transaction {
-            /**
-             * This block builds the DB query so that IF the %-wildcard was
-             * given, then BOTH name and name-less accounts are returned.
-             */
-            val query: Op<Boolean> = SqlExpressionBuilder.run {
-                val like = DemobankCustomersTable.name.like(filter)
-                /**
-                 * This IF statement is needed because Postgres would NOT
-                 * match a null column even with the %-wildcard.
-                 */
-                if (filter == "%") {
-                    return@run like.or(DemobankCustomersTable.name.isNull())
-                }
-                return@run like
-            }
-            DemobankCustomerEntity.find { query }.forEach {
-                customers.add(object {
-                    val username = it.username
-                    val name = it.name
-                    val balance = getBalanceForJson(
-                        getBalance(it.username, demobank.name),
-                        demobank.config.currency
-                    )
-                    val debitThreshold = getMaxDebitForUser(
-                        it.username,
-                        demobank.name
-                    )
-                })
-            }
-            StdOutSqlLogger
-        }
-        if (customers.size == 0) {
-            call.respond(HttpStatusCode.NoContent)
-            return@get
-        }
-        call.respond(object {val customers = customers})
-        return@get
-    }
-
-    // Change password.
-    circuitRoute.patch("/accounts/{customerUsername}/auth") {
-        val username = call.request.basicAuth()
-        val customerUsername = call.expectUriComponent("customerUsername")
-        throwIfInstitutionalName(customerUsername)
-        if (!allowOwnerOrAdmin(username, customerUsername)) throw forbidden(
-            "User $username has no rights over $customerUsername"
-        )
-        // Flow here means admin or username have the rights for this 
operation.
-        val req = call.receive<AccountPasswordChange>()
-        /**
-         * The resource/customer might still not exist, in case admin has 
requested.
-         * On the other hand, when ordinary customers request, their existence 
is checked
-         * along the basic authentication check.
-         */
-        transaction {
-            val customer = getCustomer(customerUsername) // throws 404, if not 
found.
-            customer.passwordHash = CryptoUtil.hashpw(req.new_password)
-        }
-        call.respond(HttpStatusCode.NoContent)
-        return@patch
-    }
-    // Change account (mostly contact) data.
-    circuitRoute.patch("/accounts/{resourceName}") {
-        val username = call.request.basicAuth()
-        if (username == null)
-            throw internalServerError("Authentication disabled, don't have a 
default for this request.")
-        val resourceName = call.expectUriComponent("resourceName")
-        throwIfInstitutionalName(resourceName)
-        if(!allowOwnerOrAdmin(username, resourceName)) throw forbidden(
-            "User $username has no rights over $resourceName"
-        )
-        // account found and authentication succeeded
-        val req = call.receive<CircuitAccountReconfiguration>()
-        // Only admin's allowed to change the legal name
-        if (req.name != null && username != "admin") throw forbidden(
-            "Only admin can change the user legal name"
-        )
-        if ((req.contact_data.email != null) && 
(!checkEmailAddress(req.contact_data.email)))
-            throw badRequest("Invalid e-mail address: 
${req.contact_data.email}")
-        if ((req.contact_data.phone != null) && 
(!checkPhoneNumber(req.contact_data.phone)))
-            throw badRequest("Invalid phone number: ${req.contact_data.phone}")
-        try { if (req.cashout_address != null) parsePayto(req.cashout_address) 
}
-        catch (e: InvalidPaytoError) {
-            throw badRequest("Invalid cash-out address: 
${req.cashout_address}")
-        }
-        transaction {
-            val user = getCustomer(resourceName)
-            user.email = req.contact_data.email
-            user.phone = req.contact_data.phone
-            user.cashout_address = req.cashout_address
-        }
-        call.respond(HttpStatusCode.NoContent)
-        return@patch
-    }
-    // Create new account.
-    circuitRoute.post("/accounts") {
-        call.request.basicAuth(onlyAdmin = true)
-        val req = call.receive<CircuitAccountRequest>()
-        // Validity and availability check on the input data.
-        if (req.contact_data.email != null) {
-            if (!checkEmailAddress(req.contact_data.email))
-                throw badRequest("Invalid e-mail address: 
${req.contact_data.email}.  Won't register")
-            val maybeEmailConflict = transaction {
-                DemobankCustomerEntity.find {
-                    DemobankCustomersTable.email eq req.contact_data.email
-                }.firstOrNull()
-            }
-            // Warning since two individuals claimed one same e-mail address.
-            if (maybeEmailConflict != null)
-                throw conflict("Won't register user ${req.username}: e-mail 
conflict on ${req.contact_data.email}")
-        }
-        if (req.contact_data.phone != null) {
-            if (!checkPhoneNumber(req.contact_data.phone))
-                throw badRequest("Invalid phone number: 
${req.contact_data.phone}.  Won't register")
-
-            val maybePhoneConflict = transaction {
-                DemobankCustomerEntity.find {
-                    DemobankCustomersTable.phone eq req.contact_data.phone
-                }.firstOrNull()
-            }
-            // Warning since two individuals claimed one same phone number.
-            if (maybePhoneConflict != null)
-                throw conflict("Won't register user ${req.username}: phone 
conflict on ${req.contact_data.phone}")
-        }
-        /**
-         * Check that cash-out address parses.  IBAN is not
-         * check-summed in this version; the cash-out operation
-         * just fails for invalid IBANs and the user has then
-         * the chance to update their IBAN.
-         */
-        try {
-            parsePayto(req.cashout_address)
-        }
-        catch (e: InvalidPaytoError) {
-            throw badRequest("Won't register account ${req.username}: invalid 
cash-out address: ${req.cashout_address}")
-        }
-        transaction {
-            val newAccount = insertNewAccount(
-                username = req.username,
-                password = req.password,
-                name = req.name,
-                iban = req.internal_iban,
-                demobank = ensureDemobank(call).name
-            )
-            newAccount.customer.phone = req.contact_data.phone
-            newAccount.customer.email = req.contact_data.email
-            newAccount.customer.cashout_address = req.cashout_address
-        }
-        call.respond(HttpStatusCode.NoContent)
-        return@post
-    }
-    // Get (conversion rates via) config values.
-    circuitRoute.get("/config") {
-        call.respond(ConfigResp(ratios_and_fees = ratiosAndFees))
-        return@get
-    }
-    // Only Admin and only when balance is zero.
-    circuitRoute.delete("/accounts/{resourceName}") {
-        call.request.basicAuth(onlyAdmin = true)
-        val resourceName = call.expectUriComponent("resourceName")
-        throwIfInstitutionalName(resourceName)
-        val customer = getCustomer(resourceName)
-        val bankAccount = getBankAccountFromLabel(
-            resourceName,
-            withBankFault = true // See comment "CUSTOMER AND BANK ACCOUNT 
INVARIANT".
-        )
-        val balance: BigDecimal = getBalance(bankAccount)
-        if (!isAmountZero(balance)) {
-            logger.error("Account $resourceName has $balance balance.  Won't 
delete it")
-            throw SandboxError(
-                HttpStatusCode.PreconditionFailed,
-                "Account $resourceName doesn't have zero balance.  Won't 
delete it"
-            )
-        }
-        transaction {
-            bankAccount.delete()
-            customer.delete()
-        }
-        call.respond(HttpStatusCode.NoContent)
-        return@delete
-    }
-}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/ConversionService.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/ConversionService.kt
deleted file mode 100644
index f9f9dc68..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/ConversionService.kt
+++ /dev/null
@@ -1,433 +0,0 @@
-package tech.libeufin.bank
-
-import CamtBankAccountEntry
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.client.*
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.runBlocking
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.util.*
-import java.math.BigDecimal
-import kotlin.system.exitProcess
-
-/**
- * This file contains the logic for downloading/submitting incoming/outgoing
- * fiat transactions to Nexus.  It needs the following values for operating.
- *
- * 1.  Nexus URL.
- * 2.  Credentials to authenticate at Nexus JSON API.
- * 3.  Long-polling interval.
- * 4.  Frequency of the download loop.
- *
- * Notes:
- *
- * 1.  The account to credit on incoming transactions is ALWAYS "admin".
- * 2.  The time to submit a new payment is as soon as "admin" receives one
- *     incoming regional payment.
- * 3.  At this time, Nexus does NOT offer long polling when it serves the
- *     transactions via its JSON API. => Fixed.
- * 4.  At this time, Nexus does NOT offer any filter when it serves the
- *     transactions via its JSON API. => Can be fixed by using the TWG.
- */
-
-// DEFINITIONS AND HELPERS
-
-/**
- * Timeout the HTTP client waits for the server to respond,
- * after the request is made.
- */
-val waitTimeout = 30000L
-
-/**
- * Time to wait before HTTP requesting again to the server.
- * This helps to avoid tight cycles in case the server responds
- * quickly or the client doesn't long-poll.
- */
-val newIterationTimeout = 2000L
-
-/**
- * Response format of Nexus GET /transactions.
- */
-data class TransactionItem(
-    val index: String,
-    val camtData: CamtBankAccountEntry
-)
-data class NexusTransactions(
-    val transactions: List<TransactionItem>
-)
-
-/**
- * This exception signals that the buy-in service could NOT
- * GET the list of fiat transactions from Nexus due to a client
- * error.  Because this is fatal (e.g. wrong credentials, URL not found..),
- * the service should be stopped.
- */
-class BuyinClientError : Exception()
-
-/**
- * This exception signals that POSTing a cash-out operation
- * to Nexus failed due to the client.  This is a fatal condition
- * therefore the monitor should be stopped.
- */
-class CashoutClientError : Exception()
-/**
- * Executes the 'block' function every 'loopNewReqMs' milliseconds.
- * Does not exit/fail the process upon exceptions - just logs them.
- */
-fun downloadLoop(block: () -> Unit) {
-    // Needs "runBlocking {}" to call "delay()" and in case 'block'
-    // contains suspend functions.
-    runBlocking {
-        while(true) {
-            try { block() }
-            catch (e: BuyinClientError) {
-                logger.error("The buy-in monitor had a client error while 
GETting new" +
-                        " transactions from Neuxs.  Stopping it")
-                // Rethrowing and let the caller manage it
-                throw e
-            }
-            // Tolerating any other error type that's not due to the client.
-            catch (e: Exception) {
-                logger.error("Sandbox fiat-incoming monitor excepted: 
${e.message}")
-            }
-            delay(newIterationTimeout)
-        }
-    }
-}
-
-// BUY-IN SIDE.
-
-/**
- * Applies the buy-in ratio and fees to the fiat amount
- * that came from Nexus.  The result is the regional amount
- * that will be wired to the exchange Sandbox account.
- */
-fun applyBuyinRatioAndFees(
-    amount: BigDecimal,
-    ratiosAndFees: RatioAndFees
-): BigDecimal {
-    val maybeBuyinAmount = ((amount * 
ratiosAndFees.buy_at_ratio.toBigDecimal())
-            - ratiosAndFees.buy_in_fee.toBigDecimal()).roundToTwoDigits()
-    // Bank's fault, as buying in should never lead to negative.
-    if (maybeBuyinAmount < BigDecimal.ZERO) {
-        logger.error("Negative buy-in scenario: input fiat amount was 
'${amount}'" +
-                ", buy-in ratio was '${ratiosAndFees.buy_at_ratio}'," +
-                " buy-in fee was '${ratiosAndFees.buy_in_fee}'")
-        throw internalServerError("Applying buy-in fees yielded negative 
regional amount")
-    }
-    return maybeBuyinAmount
-}
-
-private fun ensureDisabledRedirects(client: HttpClient) {
-    client.config {
-        if (followRedirects) throw Exception(
-            "HTTP client follows redirects, please disable."
-        )
-    }
-}
-/**
- * This function downloads the incoming fiat transactions from Nexus,
- * stores them into the database and triggers the related wire transfer
- * to the Taler exchange (to be specified in 'accountToCredit').  Once
- * started, this function is not supposed to return, except on _client
- * side_ errors.  On server side errors it pauses and retries.  When
- * it returns, the caller is expected to handle the error.
- */
-fun buyinMonitor(
-    demobankName: String, // used to get config values.
-    client: HttpClient,
-    accountToCredit: String,
-    accountToDebit: String = "admin"
-) {
-    ensureDisabledRedirects(client)
-    val demobank = ensureDemobank(demobankName)
-    /**
-     * Getting the config values to send authenticated requests
-     * to Nexus.  Sandbox needs one account at Nexus before being
-     * able to use these values.
-     */
-    val nexusBaseUrl = getConfigValueOrThrow(demobank.config::nexusBaseUrl)
-    val usernameAtNexus = 
getConfigValueOrThrow(demobank.config::usernameAtNexus)
-    val passwordAtNexus = 
getConfigValueOrThrow(demobank.config::passwordAtNexus)
-    /**
-     * This is the endpoint where Nexus serves all the transactions that
-     * have ingested from the fiat bank.
-     */
-    val endpoint = "bank-accounts/$usernameAtNexus/transactions"
-    val uriWithoutStart = joinUrl(nexusBaseUrl, endpoint) + 
"?long_poll_ms=$waitTimeout"
-
-    // downloadLoop does already try-catch (without failing the process).
-    downloadLoop {
-        /**
-         * This bank account will act as the debtor, once a new fiat
-         * payment is detected.  It's the debtor that pays the related
-         * regional amount to the exchange, in order to start a withdrawal
-         * operation (in regional coins).
-         */
-        val debitBankAccount = getBankAccountFromLabel(accountToDebit)
-        /**
-         * Setting the 'start' URI param in the following command
-         * lets Sandbox receive only unseen payments from Nexus.
-         */
-        val uriWithStart = 
"$uriWithoutStart&start=${debitBankAccount.lastFiatFetch}"
-        runBlocking {
-            // Maybe get new fiat transactions.
-            logger.debug("GETting fiat transactions from: $uriWithStart")
-            val resp = client.get(uriWithStart) {
-                expectSuccess = false // Avoids excepting on !2xx
-                basicAuth(usernameAtNexus, passwordAtNexus)
-            }
-            // The server failed, pause and try again
-            if (resp.status.value.toString().startsWith('5')) {
-                logger.error("Buy-in monitor requested to a failing Nexus.  
Retry.")
-                logger.error("Nexus responded: ${resp.bodyAsText()}")
-                return@runBlocking
-            }
-            // The client failed, fail the process.
-            if (resp.status.value.toString().startsWith('4')) {
-                logger.error("Buy-in monitor failed at GETting to Nexus.  
Stopping the buy-in monitor.")
-                logger.error("Nexus responded: ${resp.bodyAsText()}")
-                throw BuyinClientError()
-            }
-            // Expect 200 OK.  What if 3xx?
-            if (resp.status.value != HttpStatusCode.OK.value) {
-                logger.error("Unhandled response status ${resp.status.value}, 
failing Sandbox")
-                throw BuyinClientError()
-            }
-            // Nexus responded 200 OK, analyzing the result.
-            /**
-             * Wire to "admin" if the subject is a public key, or do
-             * nothing otherwise.
-             */
-            val respObj = jacksonObjectMapper().readValue(
-                resp.bodyAsText(),
-                NexusTransactions::class.java
-            ) // errors are logged by the caller (without failing).
-            respObj.transactions.forEach {
-                // Ignoring payments with an invalid reserved public key.
-                if 
(extractReservePubFromSubject(it.camtData.getSingletonSubject()) == null)
-                    return@forEach
-                // Extracts the amount and checks it's at most two fractional 
digits.
-                val maybeValidAmount = it.camtData.amount.value
-                if (!validatePlainAmount(maybeValidAmount)) {
-                    logger.error("Nexus gave one amount with invalid 
fractional digits: $maybeValidAmount." +
-                            "  The transaction has index ${it.index}")
-                    // Advancing the last fetched pointer, to avoid GETting
-                    // this invalid payment again.
-                    transaction {
-                        debitBankAccount.refresh()
-                        debitBankAccount.lastFiatFetch = it.index
-                    }
-                }
-                val convertedAmount = applyBuyinRatioAndFees(
-                    maybeValidAmount.toBigDecimal(),
-                    ratiosAndFees
-                )
-                transaction {
-                    wireTransfer(
-                        debitAccount = accountToDebit,
-                        creditAccount = accountToCredit,
-                        demobank = demobankName,
-                        subject = it.camtData.getSingletonSubject(),
-                        amount = "${demobank.config.currency}:$convertedAmount"
-                    )
-                    // Nexus enqueues the transactions such that the index 
increases.
-                    // If Sandbox crashes here, it'll ask again using the last 
successful
-                    // index as the start parameter.  Being this an exclusive 
bound, only
-                    // transactions later than it are expected.
-                    debitBankAccount.refresh()
-                    debitBankAccount.lastFiatFetch = it.index
-                }
-            }
-        }
-    }
-}
-
-/* DB query helper that fetches the latest cash-out operations that were
-  confirmed in the regional currency.  A cash-out operation is 'confirmed'
-  when the bank account pointed by the parameter 'bankAccountLabel' gets
-  one incoming payment.
-
-  The List return type (instead of SizedIterable) lets the caller NOT open
-  a transaction block to access the values -- although some operations _on
-  the values_ may be forbidden.
-*/
-fun getUnsubmittedTransactions(bankAccountLabel: String): 
List<BankAccountTransactionEntity> {
-    return transaction {
-        val bankAccount = getBankAccountFromLabel(bankAccountLabel)
-        val lowerExclusiveLimit = bankAccount.lastFiatSubmission?.id?.value ?: 0
-        BankAccountTransactionEntity.find {
-            BankAccountTransactionsTable.id greater lowerExclusiveLimit and (
-                BankAccountTransactionsTable.direction eq "CRDT"
-            ) and (BankAccountTransactionsTable.account eq bankAccount.id)
-        }.sortedBy { it.id }.map { it }
-        /*  The latest payment must occupy the highest index,
-        to reliably update the 'lastFiatSubmission' column of
-        the bank account. */
-    }
-}
-
-// CASH-OUT SIDE.
-
-/**
- * This function listens for regio-incoming events (LIBEUFIN_REGIO_TX)
- * on the 'watchedBankAccount' and submits the related cash-out payment
- * to Nexus.  The fiat payment will then take place ENTIRELY on Nexus'
- * responsibility.
- */
-suspend fun cashoutMonitor(
-    httpClient: HttpClient,
-    watchedBankAccount: String = "admin",
-    demobankName: String = "default", // used to get config values.
-    dbEventTimeout: Long = 0 // 0 waits forever.
-) {
-    ensureDisabledRedirects(httpClient)
-    // Register for a REGIO_TX event.
-    val eventChannel = buildChannelName(
-        NotificationsChannelDomains.LIBEUFIN_REGIO_TX,
-        watchedBankAccount
-    )
-    val objectMapper = jacksonObjectMapper()
-    val demobank = getDemobank(demobankName)
-    val bankAccount = getBankAccountFromLabel(watchedBankAccount)
-    val config = demobank?.config ?: throw internalServerError(
-        "Demobank '$demobankName' has no configuration."
-    )
-    /**
-     * The monitor needs the cash-out currency to correctly POST
-     * payment initiations at Nexus.  Recall: Nexus bank accounts
-     * do not mandate any particular currency, as they serve as mere
-     * bridges to the backing bank.  And: a backing bank may have
-     * multiple currencies, or the backing bank may not explicitly
-     * specify any currencies to be _the_ currency of the backed
-     * bank account.
-     */
-    if (config.cashoutCurrency == null) {
-        logger.error("Config lacks cash-out currency.")
-        exitProcess(1)
-    }
-    val nexusBaseUrl = getConfigValueOrThrow(config::nexusBaseUrl)
-    val usernameAtNexus = getConfigValueOrThrow(config::usernameAtNexus)
-    val passwordAtNexus = getConfigValueOrThrow(config::passwordAtNexus)
-    val paymentInitEndpoint = nexusBaseUrl.run {
-        var nexusBaseUrlFromConfig = this
-        if (!nexusBaseUrlFromConfig.endsWith('/'))
-            nexusBaseUrlFromConfig += '/'
-        /**
-         * WARNING: Nexus gives the possibility to have bank account names
-         * DIFFERENT from their owner's username.  Sandbox however MUST have
-         * its Nexus bank account named THE SAME as its username.
-         */
-        nexusBaseUrlFromConfig + 
"bank-accounts/$usernameAtNexus/payment-initiations"
-    }
-    while (true) {
-        val listenHandle = PostgresListenHandle(eventChannel)
-        // pessimistically LISTEN
-        listenHandle.postgresListen()
-        // but optimistically check for data, case some
-        // arrived _before_ the LISTEN.
-        var newTxs = getUnsubmittedTransactions(watchedBankAccount)
-        // Data found, UNLISTEN.
-        if (newTxs.isNotEmpty()) {
-            logger.debug("Found cash-out's without waiting any DB event.")
-            listenHandle.postgresUnlisten()
-        }
-        // Data not found, wait.
-        else {
-            logger.debug("Need to wait a DB event for new cash-out's")
-            val isNotificationArrived = 
listenHandle.waitOnIODispatchers(dbEventTimeout)
-            if (isNotificationArrived && listenHandle.receivedPayload == 
"CRDT")
-                newTxs = getUnsubmittedTransactions(watchedBankAccount)
-        }
-        if (newTxs.isEmpty()) {
-            logger.debug("DB event timeout expired")
-            continue
-        }
-        logger.debug("POSTing new cash-out's")
-        newTxs.forEach {
-            logger.debug("POSTing cash-out '${it.subject}' to 
$paymentInitEndpoint")
-            val body = object {
-                /**
-                 * This field is UID of the request _as assigned by the
-                 * client_.  That helps to reconcile transactions or lets
-                 * Nexus implement idempotency.  It will NOT identify the 
created
-                 * resource at the server side.  The ID of the created 
resource is
-                 * assigned _by Nexus_ and communicated in the (successful) 
response.
-                 */
-                val uid = it.accountServicerReference
-                val iban = it.creditorIban
-                val bic = it.creditorBic
-                val amount = "${config.cashoutCurrency}:${it.amount}"
-                val subject = it.subject
-                val name = it.creditorName
-            }
-            val resp = try {
-                httpClient.post(paymentInitEndpoint) {
-                    expectSuccess = false // Avoids excepting on !2xx
-                    basicAuth(usernameAtNexus, passwordAtNexus)
-                    contentType(ContentType.Application.Json)
-                    setBody(objectMapper.writeValueAsString(body))
-                }
-            }
-            // Hard-error, response did not even arrive.
-            catch (e: Exception) {
-                logger.error("Cash-out monitor could not reach Nexus.  Pause 
and retry")
-                logger.error(e.message)
-                /**
-                 * Explicit delaying because the monitor normally
-                 * waits on DB events, and this retry likely won't
-                 * wait on a DB event.
-                 */
-                delay(2000)
-                return@forEach
-            }
-            // Server fault.  Pause and retry.
-            if (resp.status.value.toString().startsWith('5')) {
-                logger.error("Cash-out monitor POSTed to a failing Nexus.  
Pause and retry")
-                logger.error("Server responded: ${resp.bodyAsText()}")
-                /**
-                 * Explicit delaying because the monitor normally
-                 * waits on DB events, and this retry likely won't
-                 * wait on a DB event.
-                 */
-                delay(2000L)
-                return@forEach
-            }
-            // Client fault, fail Sandbox.
-            if (resp.status.value.toString().startsWith('4')) {
-                logger.error("Cash-out monitor failed at POSTing to Nexus.")
-                logger.error("Nexus responded: ${resp.bodyAsText()}")
-                throw CashoutClientError()
-            }
-            // Expecting 200 OK.  What if 3xx?
-            if (resp.status.value != HttpStatusCode.OK.value) {
-                logger.error("Cash-out monitor, unhandled response status: 
${resp.status.value}.")
-                throw CashoutClientError()
-            }
-            // Successful case, mark the wire transfer as submitted,
-            // and advance the pointer to the last submitted payment.
-            val responseBody = resp.bodyAsText()
-            transaction {
-                CashoutSubmissionEntity.new {
-                    localTransaction = it.id
-                    submissionTime = resp.responseTime.timestamp
-                    /**
-                     * The following block associates the submitted payment
-                     * to the UID that Nexus assigned to it.  It is currently 
not
-                     * used in Sandbox, but might help for reconciliation.
-                     */
-                    if (responseBody.isNotEmpty())
-                        maybeNexusResposnse = responseBody
-                }
-                // Advancing the 'last submitted bookmark', to avoid
-                // handling the same transaction multiple times.
-                bankAccount.lastFiatSubmission = it
-            }
-        }
-    }
-}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/DB.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/DB.kt
deleted file mode 100644
index e20aa6ad..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/DB.kt
+++ /dev/null
@@ -1,747 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2019 Stanisci and Dold.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING.  If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-package tech.libeufin.bank
-
-import io.ktor.http.*
-import org.jetbrains.exposed.dao.Entity
-import org.jetbrains.exposed.dao.EntityClass
-import org.jetbrains.exposed.dao.IntEntity
-import org.jetbrains.exposed.dao.LongEntity
-import org.jetbrains.exposed.dao.IntEntityClass
-import org.jetbrains.exposed.dao.LongEntityClass
-import org.jetbrains.exposed.dao.id.EntityID
-import org.jetbrains.exposed.dao.id.IdTable
-import org.jetbrains.exposed.dao.id.IntIdTable
-import org.jetbrains.exposed.dao.id.LongIdTable
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.util.*
-import kotlin.reflect.*
-import kotlin.reflect.full.*
-
-/**
- * All the states to give a subscriber.
- */
-enum class SubscriberState {
-    /**
-     * No keys at all given to the bank.
-     */
-    NEW,
-
-    /**
-     * Only INI electronic message was successfully sent.
-     */
-    PARTIALLY_INITIALIZED_INI,
-
-    /**r
-     * Only HIA electronic message was successfully sent.
-     */
-    PARTIALLY_INITIALIZED_HIA,
-
-    /**
-     * Both INI and HIA were electronically sent with success.
-     */
-    INITIALIZED,
-
-    /**
-     * All the keys accounted in INI and HIA have been confirmed
-     * via physical mail.
-     */
-    READY
-}
-
-/**
- * All the states that one key can be assigned.
- */
-enum class KeyState {
-
-    /**
-     * The key was never communicated.
-     */
-    MISSING,
-
-    /**
-     * The key has been electronically sent.
-     */
-    NEW,
-
-    /**
-     * The key has been confirmed (either via physical mail
-     * or electronically -- e.g. with certificates)
-     */
-    RELEASED
-}
-
-/**
- * Stores one config object to the database.  Each field
- * name and value populate respectively the configKey and
- * configValue columns.  Rows are defined in the following way:
- * demobankName | configKey | configValue
- */
-fun insertConfigPairs(config: DemobankConfig, override: Boolean = false) {
-    // Fill the config key-value pairs in the DB.
-    config::class.declaredMemberProperties.forEach { configField ->
-        val maybeValue = configField.getter.call(config)
-        if (override) {
-            val maybeConfigPair = DemobankConfigPairEntity.find {
-                DemobankConfigPairsTable.configKey eq configField.name
-            }.firstOrNull()
-            if (maybeConfigPair == null)
-                throw internalServerError("Cannot override config value 
'${configField.name}' not found.")
-            maybeConfigPair.configValue = maybeValue?.toString()
-            return@forEach
-        }
-        DemobankConfigPairEntity.new {
-            this.demobankName = config.demobankName
-            this.configKey = configField.name
-            this.configValue = maybeValue?.toString()
-        }
-    }
-}
-
-object DemobankConfigPairsTable : LongIdTable() {
-    val demobankName = text("demobankName")
-    val configKey = text("configKey")
-    val configValue = text("configValue").nullable()
-}
-
-class DemobankConfigPairEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<DemobankConfigPairEntity>(DemobankConfigPairsTable)
-    var demobankName by DemobankConfigPairsTable.demobankName
-    var configKey by DemobankConfigPairsTable.configKey
-    var configValue by DemobankConfigPairsTable.configValue
-}
-
-object DemobankConfigsTable : LongIdTable() {
-    val name = text("hostname")
-}
-
-// Helpers for handling config values in memory.
-typealias DemobankConfigKey = String
-typealias DemobankConfigValue = String?
-fun Pair<DemobankConfigKey, DemobankConfigValue>.expectValue(): String {
-    if (this.second == null) throw internalServerError("Config value for 
'${this.first}' is null in the database.")
-    return this.second as String
-}
-
-class DemobankConfigEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<DemobankConfigEntity>(DemobankConfigsTable)
-    var name by DemobankConfigsTable.name
-    /**
-     * This object gets defined by parsing all the configuration
-     * values found in the DB for one demobank.  Those values are
-     * retrieved from _another_ table.
-     */
-    val config: DemobankConfig by lazy {
-        // Getting all the values for this demobank.
-        val configPairs: List<Pair<DemobankConfigKey, DemobankConfigValue>> = 
transaction {
-            val maybeConfigPairs = DemobankConfigPairEntity.find {
-                DemobankConfigPairsTable.demobankName.eq(name)
-            }
-            if (maybeConfigPairs.empty()) throw SandboxError(
-                HttpStatusCode.InternalServerError,
-                "No config values of $name were found in the database"
-            )
-            // Copying results to a DB-agnostic list, to later operate out of 
"transaction {}"
-            maybeConfigPairs.map { Pair(it.configKey, it.configValue) }
-        }
-        // Building the args to instantiate a DemobankConfig (non-Exposed) 
object.
-        val args = mutableMapOf<KParameter, Any?>()
-        // For each constructor parameter name, find the same-named database 
entry.
-        val configClass = DemobankConfig::class
-        if (configClass.primaryConstructor == null) {
-            throw SandboxError(
-                HttpStatusCode.InternalServerError,
-                "${configClass.simpleName} primaryConstructor is null."
-            )
-        }
-        if (configClass.primaryConstructor?.parameters == null) {
-            throw SandboxError(
-                HttpStatusCode.InternalServerError,
-                "${configClass.simpleName} primaryConstructor" +
-                        " arguments is null.  Cannot set any config value."
-            )
-        }
-        // For each field in the config object, find the respective DB row.
-        configClass.primaryConstructor?.parameters?.forEach { par: KParameter 
->
-            val configPairFromDb: Pair<DemobankConfigKey, DemobankConfigValue>?
-            = configPairs.firstOrNull {
-                    configPair: Pair<DemobankConfigKey, DemobankConfigValue> ->
-                configPair.first == par.name
-            }
-            if (configPairFromDb == null) {
-                throw SandboxError(
-                    HttpStatusCode.InternalServerError,
-                    "Config key '${par.name}' not found in the database."
-                )
-            }
-            when(par.type) {
-                // non-nullable
-                typeOf<Boolean>() -> { args[par] = 
configPairFromDb.expectValue().toBoolean() }
-                typeOf<Int>() -> { args[par] = 
configPairFromDb.expectValue().toInt() }
-                // nullable
-                typeOf<Boolean?>() -> { args[par] = 
configPairFromDb.second?.toBoolean() }
-                typeOf<Int?>() -> { args[par] = 
configPairFromDb.second?.toInt() }
-                else -> args[par] = configPairFromDb.second
-            }
-        }
-        // Proceeding now to instantiate the config class, and make it a field 
of this type.
-        configClass.primaryConstructor!!.callBy(args)
-    }
-}
-
-/**
- * Users who are allowed to log into the demo bank.
- * Created via the /demobanks/{demobankname}/register endpoint.
- */
-object DemobankCustomersTable : LongIdTable() {
-    val username = text("username")
-    val passwordHash = text("passwordHash")
-    val name = text("name").nullable()
-    val email = text("email").nullable()
-    val phone = text("phone").nullable()
-    val cashout_address = text("cashout_address").nullable()
-}
-
-class DemobankCustomerEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<DemobankCustomerEntity>(DemobankCustomersTable)
-    var username by DemobankCustomersTable.username
-    var passwordHash by DemobankCustomersTable.passwordHash
-    var name by DemobankCustomersTable.name
-    var email by DemobankCustomersTable.email
-    var phone by DemobankCustomersTable.phone
-    var cashout_address by DemobankCustomersTable.cashout_address
-}
-
-/**
- * This table stores RSA public keys of subscribers.
- */
-object EbicsSubscriberPublicKeysTable : IntIdTable() {
-    val rsaPublicKey = blob("rsaPublicKey")
-    val state = enumeration("state", KeyState::class)
-}
-
-class EbicsSubscriberPublicKeyEntity(id: EntityID<Int>) : IntEntity(id) {
-    companion object : 
IntEntityClass<EbicsSubscriberPublicKeyEntity>(EbicsSubscriberPublicKeysTable)
-    var rsaPublicKey by EbicsSubscriberPublicKeysTable.rsaPublicKey
-    var state by EbicsSubscriberPublicKeysTable.state
-}
-
-/**
- * Ebics 'host'(s) that are served by one Sandbox instance.
- */
-object EbicsHostsTable : IntIdTable() {
-    val hostID = text("hostID")
-    val ebicsVersion = text("ebicsVersion")
-    val signaturePrivateKey = blob("signaturePrivateKey")
-    val encryptionPrivateKey = blob("encryptionPrivateKey")
-    val authenticationPrivateKey = blob("authenticationPrivateKey")
-}
-
-class EbicsHostEntity(id: EntityID<Int>) : IntEntity(id) {
-    companion object : IntEntityClass<EbicsHostEntity>(EbicsHostsTable)
-    var hostId by EbicsHostsTable.hostID
-    var ebicsVersion by EbicsHostsTable.ebicsVersion
-    var signaturePrivateKey by EbicsHostsTable.signaturePrivateKey
-    var encryptionPrivateKey by EbicsHostsTable.encryptionPrivateKey
-    var authenticationPrivateKey by EbicsHostsTable.authenticationPrivateKey
-}
-
-/**
- * Ebics Subscribers table.
- */
-object EbicsSubscribersTable : IntIdTable() {
-    val userId = text("userID")
-    val partnerId = text("partnerID")
-    val systemId = text("systemID").nullable()
-    val hostId = text("hostID")
-    val signatureKey = reference("signatureKey", 
EbicsSubscriberPublicKeysTable).nullable()
-    val encryptionKey = reference("encryptionKey", 
EbicsSubscriberPublicKeysTable).nullable()
-    val authenticationKey = reference("authorizationKey", 
EbicsSubscriberPublicKeysTable).nullable()
-    val nextOrderID = integer("nextOrderID")
-    val state = enumeration("state", SubscriberState::class)
-    val bankAccount = reference(
-        "bankAccount",
-        BankAccountsTable,
-        onDelete = ReferenceOption.CASCADE
-    ).nullable()
-}
-
-class EbicsSubscriberEntity(id: EntityID<Int>) : IntEntity(id) {
-    companion object : 
IntEntityClass<EbicsSubscriberEntity>(EbicsSubscribersTable)
-    var userId by EbicsSubscribersTable.userId
-    var partnerId by EbicsSubscribersTable.partnerId
-    var systemId by EbicsSubscribersTable.systemId
-    var hostId by EbicsSubscribersTable.hostId
-    var signatureKey by EbicsSubscriberPublicKeyEntity optionalReferencedOn 
EbicsSubscribersTable.signatureKey
-    var encryptionKey by EbicsSubscriberPublicKeyEntity optionalReferencedOn 
EbicsSubscribersTable.encryptionKey
-    var authenticationKey by EbicsSubscriberPublicKeyEntity 
optionalReferencedOn EbicsSubscribersTable.authenticationKey
-    var nextOrderID by EbicsSubscribersTable.nextOrderID
-    var state by EbicsSubscribersTable.state
-    var bankAccount by BankAccountEntity optionalReferencedOn 
EbicsSubscribersTable.bankAccount
-}
-
-/**
- * Details of a download order.
- */
-object EbicsDownloadTransactionsTable : IdTable<String>() {
-    override val id = text("transactionID").entityId()
-    val orderType = text("orderType")
-    val host = reference("host", EbicsHostsTable)
-    val subscriber = reference("subscriber", EbicsSubscribersTable)
-    val encodedResponse = text("encodedResponse")
-    val transactionKeyEnc = blob("transactionKeyEnc")
-    val numSegments = integer("numSegments")
-    val segmentSize = integer("segmentSize")
-    val receiptReceived = bool("receiptReceived")
-}
-
-class EbicsDownloadTransactionEntity(id: EntityID<String>) : 
Entity<String>(id) {
-    companion object : EntityClass<String, 
EbicsDownloadTransactionEntity>(EbicsDownloadTransactionsTable)
-
-    var orderType by EbicsDownloadTransactionsTable.orderType
-    var host by EbicsHostEntity referencedOn 
EbicsDownloadTransactionsTable.host
-    var subscriber by EbicsSubscriberEntity referencedOn 
EbicsDownloadTransactionsTable.subscriber
-    var encodedResponse by EbicsDownloadTransactionsTable.encodedResponse
-    var numSegments by EbicsDownloadTransactionsTable.numSegments
-    var transactionKeyEnc by EbicsDownloadTransactionsTable.transactionKeyEnc
-    var segmentSize by EbicsDownloadTransactionsTable.segmentSize
-    var receiptReceived by EbicsDownloadTransactionsTable.receiptReceived
-}
-
-/**
- * Details of a upload order.
- */
-object EbicsUploadTransactionsTable : IdTable<String>() {
-    override val id = text("transactionID").entityId()
-    val orderType = text("orderType")
-    val orderID = text("orderID")
-    val host = reference("host", EbicsHostsTable)
-    val subscriber = reference("subscriber", EbicsSubscribersTable)
-    val numSegments = integer("numSegments")
-    val lastSeenSegment = integer("lastSeenSegment")
-    val transactionKeyEnc = blob("transactionKeyEnc")
-}
-
-class EbicsUploadTransactionEntity(id: EntityID<String>) : Entity<String>(id) {
-    companion object : EntityClass<String, 
EbicsUploadTransactionEntity>(EbicsUploadTransactionsTable)
-    var orderType by EbicsUploadTransactionsTable.orderType
-    var orderID by EbicsUploadTransactionsTable.orderID
-    var host by EbicsHostEntity referencedOn EbicsUploadTransactionsTable.host
-    var subscriber by EbicsSubscriberEntity referencedOn 
EbicsUploadTransactionsTable.subscriber
-    var numSegments by EbicsUploadTransactionsTable.numSegments
-    var lastSeenSegment by EbicsUploadTransactionsTable.lastSeenSegment
-    var transactionKeyEnc by EbicsUploadTransactionsTable.transactionKeyEnc
-}
-
-/**
- * FIXME: document this.
- */
-object EbicsOrderSignaturesTable : IntIdTable() {
-    val orderID = text("orderID")
-    val orderType = text("orderType")
-    val partnerID = text("partnerID")
-    val userID = text("userID")
-    val signatureAlgorithm = text("signatureAlgorithm")
-    val signatureValue = blob("signatureValue")
-}
-
-class EbicsOrderSignatureEntity(id: EntityID<Int>) : IntEntity(id) {
-    companion object : 
IntEntityClass<EbicsOrderSignatureEntity>(EbicsOrderSignaturesTable)
-    var orderID by EbicsOrderSignaturesTable.orderID
-    var orderType by EbicsOrderSignaturesTable.orderType
-    var partnerID by EbicsOrderSignaturesTable.partnerID
-    var userID by EbicsOrderSignaturesTable.userID
-    var signatureAlgorithm by EbicsOrderSignaturesTable.signatureAlgorithm
-    var signatureValue by EbicsOrderSignaturesTable.signatureValue
-}
-
-/**
- * FIXME: document this.
- */
-object EbicsUploadTransactionChunksTable : IdTable<String>() {
-    override val id = text("transactionID").entityId()
-    val chunkIndex = integer("chunkIndex")
-    val chunkContent = blob("chunkContent")
-}
-
-// FIXME: Is upload chunking not implemented somewhere?!
-class EbicsUploadTransactionChunkEntity(id: EntityID<String>) : 
Entity<String>(id) {
-    companion object : EntityClass<String, 
EbicsUploadTransactionChunkEntity>(EbicsUploadTransactionChunksTable)
-    var chunkIndex by EbicsUploadTransactionChunksTable.chunkIndex
-    var chunkContent by EbicsUploadTransactionChunksTable.chunkContent
-}
-
-
-/**
- * Holds those transactions that aren't yet reported in a Camt.053 document.
- * After reporting those, the table gets emptied.  Rows are merely references
- * to the main ledger.
- */
-object BankAccountFreshTransactionsTable : LongIdTable() {
-    val transactionRef = reference(
-        "transaction",
-        BankAccountTransactionsTable,
-        onDelete = ReferenceOption.CASCADE
-    )
-}
-class BankAccountFreshTransactionEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<BankAccountFreshTransactionEntity>(BankAccountFreshTransactionsTable)
-    var transactionRef by BankAccountTransactionEntity referencedOn 
BankAccountFreshTransactionsTable.transactionRef
-}
-
-/**
- * Table that keeps all the payments initiated by PAIN.001.
- */
-object BankAccountTransactionsTable : LongIdTable() {
-    val creditorIban = text("creditorIban")
-    val creditorBic = text("creditorBic").nullable()
-    val creditorName = text("creditorName")
-    val debtorIban = text("debtorIban")
-    val debtorBic = text("debtorBic").nullable()
-    val debtorName = text("debtorName")
-    val subject = text("subject")
-    // Amount is a BigDecimal in String form.
-    val amount = text("amount")
-    val currency = text("currency")
-    // Milliseconds since the Epoch.
-    val date = long("date")
-
-    /**
-     * UID assigned to the payment by Sandbox.  Despite the camt-looking
-     * name, this UID is always given, even when no EBICS or camt are being
-     * served.
-     */
-    val accountServicerReference = text("accountServicerReference")
-    /**
-     * The following two values are pain.001 specific.  Sandbox stores
-     * them when it serves EBICS connections.
-     */
-    val pmtInfId = text("pmtInfId").nullable()
-    val endToEndId = text("EndToEndId").nullable()
-    val direction = text("direction")
-    /**
-     * Bank account of the party whose 'direction' refers.  This version allows
-     * only both parties to be registered at the running Sandbox.
-     */
-    val account = reference(
-        "account", BankAccountsTable,
-        onDelete = ReferenceOption.CASCADE
-    )
-    // Redundantly storing the demobank for query convenience.
-    val demobank = reference("demobank", DemobankConfigsTable)
-}
-
-class BankAccountTransactionEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<BankAccountTransactionEntity>(BankAccountTransactionsTable) {
-        override fun new(init: BankAccountTransactionEntity.() -> Unit): 
BankAccountTransactionEntity {
-            /**
-             * Fresh transactions are those that wait to be included in a
-             * "history" report, likely a Camt.5x message.  The "fresh 
transactions"
-             * table keeps a list of such transactions.
-             */
-            val freshTx = super.new(init)
-            BankAccountFreshTransactionsTable.insert {
-                it[transactionRef] = freshTx.id
-            }
-            /**
-             * The bank account involved in this transaction points to
-             * it as the "last known" transaction, to make it easier to
-             * build histories that depend on such record.
-             */
-            freshTx.account.lastTransaction = freshTx
-            return freshTx
-        }
-    }
-    var creditorIban by BankAccountTransactionsTable.creditorIban
-    var creditorBic by BankAccountTransactionsTable.creditorBic
-    var creditorName by BankAccountTransactionsTable.creditorName
-    var debtorIban by BankAccountTransactionsTable.debtorIban
-    var debtorBic by BankAccountTransactionsTable.debtorBic
-    var debtorName by BankAccountTransactionsTable.debtorName
-    var subject by BankAccountTransactionsTable.subject
-    var amount by BankAccountTransactionsTable.amount
-    var currency by BankAccountTransactionsTable.currency
-    var date by BankAccountTransactionsTable.date
-    var accountServicerReference by 
BankAccountTransactionsTable.accountServicerReference
-    var pmtInfId by BankAccountTransactionsTable.pmtInfId
-    var endToEndId by BankAccountTransactionsTable.endToEndId
-    var direction by BankAccountTransactionsTable.direction
-    var account by BankAccountEntity referencedOn 
BankAccountTransactionsTable.account
-    var demobank by DemobankConfigEntity referencedOn 
BankAccountTransactionsTable.demobank
-}
-
-/**
- * Table that keeps information about which bank accounts (iban+bic+name)
- * are active in the system.  In the current version, 'label' and 'owner'
- * are always equal; future versions may change this, when one customer can
- * own multiple bank accounts.
- */
-object BankAccountsTable : IntIdTable() {
-    val balance = text("balance").default("0")
-    val iban = text("iban")
-    val bic = text("bic").default("SANDBOXX")
-    val label = text("label").uniqueIndex("accountLabelIndex")
-    /**
-     * This field is the username of the customer that owns the
-     * bank account.  Admin is the only exception: that can specify
-     * this field as "admin" although no customer backs it.
-     */
-    val owner = text("owner")
-    val isPublic = bool("isPublic").default(false)
-    val demoBank = reference("demoBank", DemobankConfigsTable)
-
-    /**
-     * Point to the last transaction related to this account, regardless
-     * of it being credit or debit.  This reference helps to construct
-     * history results that start from / depend on the last transaction.
-     */
-    val lastTransaction = reference("lastTransaction", 
BankAccountTransactionsTable).nullable()
-
-    /**
-     * Points to the transaction that was last submitted by the conversion
-     * service to Nexus, in order to initiate a fiat payment related to a
-     * cash-out operation.
-     */
-    val lastFiatSubmission = reference("lastFiatSubmission", 
BankAccountTransactionsTable).nullable()
-
-    /**
-     * Tracks the last fiat payment that was read from Nexus.  This tracker
-     * gets updated ONLY IF the exchange gets successfully paid with the 
related
-     * amount in the regional currency.
-     */
-    val lastFiatFetch = text("lastFiatFetch").default("0")
-}
-
-class BankAccountEntity(id: EntityID<Int>) : IntEntity(id) {
-    companion object : IntEntityClass<BankAccountEntity>(BankAccountsTable)
-
-    var balance by BankAccountsTable.balance
-    var iban by BankAccountsTable.iban
-    var bic by BankAccountsTable.bic
-    var label by BankAccountsTable.label
-    var owner by BankAccountsTable.owner
-    var isPublic by BankAccountsTable.isPublic
-    var demoBank by DemobankConfigEntity referencedOn 
BankAccountsTable.demoBank
-    var lastTransaction by BankAccountTransactionEntity optionalReferencedOn 
BankAccountsTable.lastTransaction
-    var lastFiatSubmission by BankAccountTransactionEntity 
optionalReferencedOn BankAccountsTable.lastFiatSubmission
-    var lastFiatFetch by BankAccountsTable.lastFiatFetch
-}
-
-object BankAccountStatementsTable : IntIdTable() {
-    val statementId = text("statementId")
-    val creationTime = long("creationTime")
-    val xmlMessage = text("xmlMessage")
-    val bankAccount = reference("bankAccount", BankAccountsTable)
-    // Signed BigDecimal representing a Camt.053 CLBD field.
-    val balanceClbd = text("balanceClbd").nullable()
-}
-
-class BankAccountStatementEntity(id: EntityID<Int>) : IntEntity(id) {
-    companion object : 
IntEntityClass<BankAccountStatementEntity>(BankAccountStatementsTable)
-    var statementId by BankAccountStatementsTable.statementId
-    var creationTime by BankAccountStatementsTable.creationTime
-    var xmlMessage by BankAccountStatementsTable.xmlMessage
-    var bankAccount by BankAccountEntity referencedOn 
BankAccountStatementsTable.bankAccount
-    var balanceClbd by BankAccountStatementsTable.balanceClbd
-}
-
-enum class CashoutOperationStatus { CONFIRMED, PENDING }
-object CashoutOperationsTable : LongIdTable() {
-    val uuid = uuid("uuid").autoGenerate()
-    /**
-     * This amount is the one the user entered in the cash-out
-     * dialog.  That will show up as the outgoing transfer in their
-     * local currency bank account.
-     */
-    val amountDebit = text("amountDebit")
-    val amountCredit = text("amountCredit")
-    val buyAtRatio = text("buyAtRatio")
-    val buyInFee = text("buyInFee")
-    val sellAtRatio = text("sellAtRatio")
-    val sellOutFee = text("sellOutFee")
-    val subject = text("subject")
-    val creationTime = long("creationTime") // in milliseconds.
-    val confirmationTime = long("confirmationTime").nullable() // in 
milliseconds.
-    val tanChannel = enumeration("tanChannel", SupportedTanChannels::class)
-    val account = text("account")
-    val cashoutAddress = text("cashoutAddress")
-    val tan = text("tan")
-    val status = enumeration("status", 
CashoutOperationStatus::class).default(CashoutOperationStatus.PENDING)
-}
-
-class CashoutOperationEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<CashoutOperationEntity>(CashoutOperationsTable)
-    var uuid by CashoutOperationsTable.uuid
-    var amountDebit by CashoutOperationsTable.amountDebit
-    var amountCredit by CashoutOperationsTable.amountCredit
-    var buyAtRatio by CashoutOperationsTable.buyAtRatio
-    var buyInFee by CashoutOperationsTable.buyInFee
-    var sellAtRatio by CashoutOperationsTable.sellAtRatio
-    var sellOutFee by CashoutOperationsTable.sellOutFee
-    var subject by CashoutOperationsTable.subject
-    var creationTime by CashoutOperationsTable.creationTime
-    var confirmationTime by CashoutOperationsTable.confirmationTime
-    var tanChannel by CashoutOperationsTable.tanChannel
-    var account by CashoutOperationsTable.account
-    var cashoutAddress by CashoutOperationsTable.cashoutAddress
-    var tan by CashoutOperationsTable.tan
-    var status by CashoutOperationsTable.status
-}
-object TalerWithdrawalsTable : LongIdTable() {
-    val wopid = uuid("wopid").autoGenerate()
-    val amount = text("amount") // $currency:x.y
-    /**
-     * Turns to true after the wallet gave the reserve public key
-     * and the exchange details to the bank.
-     */
-    val selectionDone = bool("selectionDone").default(false)
-    val aborted = bool("aborted").default(false)
-    /**
-     * Turns to true after the wire transfer to the exchange bank account
-     * gets completed _on the bank's side_.  This does never guarantees that
-     * the payment arrived at the exchange's bank yet.
-     */
-    val confirmationDone = bool("confirmationDone").default(false)
-    val reservePub = text("reservePub").nullable()
-    val selectedExchangePayto = text("selectedExchangePayto").nullable()
-    val walletBankAccount = reference("walletBankAccount", BankAccountsTable)
-}
-class TalerWithdrawalEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<TalerWithdrawalEntity>(TalerWithdrawalsTable)
-    var wopid by TalerWithdrawalsTable.wopid
-    var selectionDone by TalerWithdrawalsTable.selectionDone
-    var confirmationDone by TalerWithdrawalsTable.confirmationDone
-    var reservePub by TalerWithdrawalsTable.reservePub
-    var selectedExchangePayto by TalerWithdrawalsTable.selectedExchangePayto
-    var amount by TalerWithdrawalsTable.amount
-    var walletBankAccount by BankAccountEntity referencedOn 
TalerWithdrawalsTable.walletBankAccount
-    var aborted by TalerWithdrawalsTable.aborted
-}
-
-object BankAccountReportsTable : IntIdTable() {
-    val reportId = text("reportId")
-    val creationTime = long("creationTime")
-    val xmlMessage = text("xmlMessage")
-    val bankAccount = reference("bankAccount", BankAccountsTable)
-}
-
-/**
- * This table tracks the cash-out requests that Sandbox sends to Nexus.
- * Only successful requests make it to this table.  Failed request would
- * either _stop_ the conversion service (for client-side errors) or get retried
- * at a later time (for server-side errors.)
- */
-object CashoutSubmissionsTable: LongIdTable() {
-    val localTransaction = reference("localTransaction", 
BankAccountTransactionsTable).uniqueIndex()
-    val maybeNexusResponse = text("maybeNexusResponse").nullable()
-    val submissionTime = long("submissionTime").nullable() // failed don't 
have it.
-}
-
-class CashoutSubmissionEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<CashoutSubmissionEntity>(CashoutSubmissionsTable)
-    var localTransaction by CashoutSubmissionsTable.localTransaction
-    var maybeNexusResposnse by CashoutSubmissionsTable.maybeNexusResponse
-    var submissionTime by CashoutSubmissionsTable.submissionTime
-}
-
-fun dbDropTables(connStringFromEnv: String) {
-    connectWithSchema(getJdbcConnectionFromPg(connStringFromEnv))
-    if (isPostgres()) {
-        val ret = execCommand(
-            listOf(
-                "libeufin-load-sql",
-                "-d",
-                connStringFromEnv,
-                "-s",
-                "sandbox",
-                "-r" // the drop option
-            ),
-            /**
-             * Tolerating a failure here helps to manage the case
-             * where an empty database is attempted to be dropped.
-             */
-            throwIfFails = false
-        )
-        if (ret != 0)
-            logger.warn("Dropping the sandbox tables failed.  Was the DB 
filled before?")
-        return
-    }
-    transaction {
-        SchemaUtils.drop(
-            CashoutSubmissionsTable,
-            EbicsSubscribersTable,
-            EbicsSubscriberPublicKeysTable,
-            EbicsHostsTable,
-            EbicsDownloadTransactionsTable,
-            EbicsUploadTransactionsTable,
-            EbicsUploadTransactionChunksTable,
-            EbicsOrderSignaturesTable,
-            BankAccountTransactionsTable,
-            BankAccountFreshTransactionsTable,
-            BankAccountsTable,
-            BankAccountReportsTable,
-            BankAccountStatementsTable,
-            DemobankConfigsTable,
-            DemobankConfigPairsTable,
-            TalerWithdrawalsTable,
-            DemobankCustomersTable,
-            CashoutOperationsTable
-        )
-    }
-
-}
-
-fun dbCreateTables(connStringFromEnv: String) {
-    connectWithSchema(getJdbcConnectionFromPg(connStringFromEnv))
-    if (isPostgres()) {
-        execCommand(listOf(
-            "libeufin-load-sql",
-            "-d",
-            connStringFromEnv,
-            "-s",
-            "sandbox"
-        ))
-        return
-    }
-    // Still using the legacy way for other DBMSs, like SQLite.
-    transaction {
-        SchemaUtils.create(
-            CashoutSubmissionsTable,
-            DemobankConfigsTable,
-            DemobankConfigPairsTable,
-            EbicsSubscribersTable,
-            EbicsSubscriberPublicKeysTable,
-            EbicsHostsTable,
-            EbicsDownloadTransactionsTable,
-            EbicsUploadTransactionsTable,
-            EbicsUploadTransactionChunksTable,
-            EbicsOrderSignaturesTable,
-            BankAccountTransactionsTable,
-            BankAccountFreshTransactionsTable,
-            BankAccountsTable,
-            BankAccountReportsTable,
-            BankAccountStatementsTable,
-            TalerWithdrawalsTable,
-            DemobankCustomersTable,
-            CashoutOperationsTable
-        )
-    }
-}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index ba370c3b..799788c0 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -14,26 +14,29 @@ data class Customer(
     val login: String,
     val passwordHash: String,
     val name: String,
-    val email: String,
-    val phone: String,
-    val cashoutPayto: String,
-    val cashoutCurrency: String
+    val dbRowId: Long? = null, // mostly used when retrieving records.
+    val email: String?,
+    val phone: String?,
+    val cashoutPayto: String?,
+    val cashoutCurrency: String?
 )
+fun Customer.expectRowId(): Long = this.dbRowId ?: throw 
internalServerError("Cutsomer '${this.login}' had no DB row ID")
 
 data class TalerAmount(
     val value: Long,
     val frac: Int
 )
 
+// BIC got removed, because it'll be expressed in the internal_payto_uri.
 data class BankAccount(
-    val iban: String,
-    val bic: String,
-    val bankAccountLabel: String,
+    val internalPaytoUri: String,
     val owningCustomerId: Long,
     val isPublic: Boolean = false,
-    val lastNexusFetchRowId: Long,
+    val isTalerExchange: Boolean = false,
+    val lastNexusFetchRowId: Long = 0L,
     val balance: TalerAmount? = null,
-    val hasDebt: Boolean
+    val hasDebt: Boolean,
+    val maxDebt: TalerAmount
 )
 
 enum class TransactionDirection {
@@ -44,6 +47,25 @@ enum class TanChannel {
     sms, email, file
 }
 
+enum class TokenScope {
+    readonly, readwrite
+}
+
+data class BearerToken(
+    val content: ByteArray,
+    val scope: TokenScope,
+    val creationTime: Long,
+    val expirationTime: Long,
+    /**
+     * Serial ID of the database row that hosts the bank customer
+     * that is associated with this token.  NOTE: if the token is
+     * refreshed by a client that doesn't have a user+password login
+     * in the system, the creator remains always the original bank
+     * customer that created the very first token.
+     */
+    val bankCustomer: Long
+)
+
 data class BankInternalTransaction(
     val creditorAccountId: Long,
     val debtorAccountId: Long,
@@ -56,11 +78,9 @@ data class BankInternalTransaction(
 )
 
 data class BankAccountTransaction(
-    val creditorIban: String,
-    val creditorBic: String,
+    val creditorPaytoUri: String,
     val creditorName: String,
-    val debtorIban: String,
-    val debtorBic: String,
+    val debtorPaytoUri: String,
     val debtorName: String,
     val subject: String,
     val amount: TalerAmount,
@@ -98,7 +118,7 @@ data class Cashout(
     val tanChannel: TanChannel,
     val tanCode: String,
     val bankAccount: Long,
-    val cashoutAddress: String,
+    val credit_payto_uri: String,
     val cashoutCurrency: String
 )
 
@@ -170,7 +190,15 @@ class Database(private val dbConfig: String) {
     }
 
     // CUSTOMERS
-    fun customerCreate(customer: Customer): Boolean {
+    /**
+     * This method INSERTs a new customer into the database and
+     * returns its row ID.  That is useful because often a new user
+     * ID has to be specified in more database records, notably in
+     * bank accounts to point at their owners.
+     *
+     * In case of conflict, this method returns null.
+     */
+    fun customerCreate(customer: Customer): Long? {
         reconnect()
         val stmt = prepare("""
             INSERT INTO customers (
@@ -182,7 +210,8 @@ class Database(private val dbConfig: String) {
               ,cashout_payto
               ,cashout_currency
             )
-            VALUES (?, ?, ?, ?, ?, ?, ?) 
+            VALUES (?, ?, ?, ?, ?, ?, ?)
+            RETURNING customer_id
         """
         )
         stmt.setString(1, customer.login)
@@ -193,12 +222,84 @@ class Database(private val dbConfig: String) {
         stmt.setString(6, customer.cashoutPayto)
         stmt.setString(7, customer.cashoutCurrency)
 
-        return myExecute(stmt)
+        val res = try {
+            stmt.executeQuery()
+        } catch (e: SQLException) {
+            logger.error(e.message)
+            if (e.errorCode == 0) return null // unique constraint violation.
+            throw e // rethrow on other errors.
+        }
+        res.use {
+            if (!it.next())
+                throw internalServerError("SQL RETURNING gave nothing.")
+            return it.getLong("customer_id")
+        }
+    }
+
+    fun customerPwAuth(login: String, pwHash: String): Customer? {
+        reconnect()
+        val stmt = prepare("""
+            SELECT
+              name,
+              email,
+              phone,
+              cashout_payto,
+              cashout_currency
+            FROM customers
+            WHERE login=? AND password_hash=?
+        """)
+        stmt.setString(1, login)
+        stmt.setString(2, pwHash)
+        val rs = stmt.executeQuery()
+        rs.use {
+            if (!rs.next()) return null
+            return Customer(
+                login = login,
+                passwordHash = pwHash,
+                name = it.getString("name"),
+                phone = it.getString("phone"),
+                email = it.getString("email"),
+                cashoutCurrency = it.getString("cashout_currency"),
+                cashoutPayto = it.getString("cashout_payto")
+            )
+        }
+    }
+
+    // Mostly used to get customers out of bearer tokens.
+    fun customerGetFromRowId(customer_id: Long): Customer? {
+        reconnect()
+        val stmt = prepare("""
+            SELECT
+              login,
+              password_hash,
+              name,
+              email,
+              phone,
+              cashout_payto,
+              cashout_currency
+            FROM customers
+            WHERE customer_id=?
+        """)
+        stmt.setLong(1, customer_id)
+        val rs = stmt.executeQuery()
+        rs.use {
+            if (!rs.next()) return null
+            return Customer(
+                login = it.getString("login"),
+                passwordHash = it.getString("password_hash"),
+                name = it.getString("name"),
+                phone = it.getString("phone"),
+                email = it.getString("email"),
+                cashoutCurrency = it.getString("cashout_currency"),
+                cashoutPayto = it.getString("cashout_payto")
+            )
+        }
     }
     fun customerGetFromLogin(login: String): Customer? {
         reconnect()
         val stmt = prepare("""
             SELECT
+              customer_id,
               password_hash,
               name,
               email,
@@ -219,84 +320,140 @@ class Database(private val dbConfig: String) {
                 phone = it.getString("phone"),
                 email = it.getString("email"),
                 cashoutCurrency = it.getString("cashout_currency"),
-                cashoutPayto = it.getString("cashout_payto")
+                cashoutPayto = it.getString("cashout_payto"),
+                dbRowId = it.getLong("customer_id")
             )
         }
     }
     // Possibly more "customerGetFrom*()" to come.
 
+    // BEARER TOKEN
+    fun bearerTokenCreate(token: BearerToken): Boolean {
+        reconnect()
+        val stmt = prepare("""
+             INSERT INTO bearer_tokens
+               (content,
+                creation_time,
+                expiration_time,
+                scope,
+                bank_customer              
+               ) VALUES
+               (?, ?, ?, ?::token_scope_enum, ?)
+        """)
+        stmt.setBytes(1, token.content)
+        stmt.setLong(2, token.creationTime)
+        stmt.setLong(3, token.expirationTime)
+        stmt.setString(4, token.scope.name)
+        stmt.setLong(5, token.bankCustomer)
+
+        return myExecute(stmt)
+    }
+    fun bearerTokenGet(token: ByteArray): BearerToken? {
+        reconnect()
+        val stmt = prepare("""
+            SELECT
+              expiration_time,
+              creation_time,
+              bank_customer,
+              scope
+            FROM bearer_tokens
+            WHERE content=?;            
+        """)
+
+        stmt.setBytes(1, token)
+        stmt.executeQuery().use {
+            if (!it.next()) return null
+            return BearerToken(
+                content = token,
+                creationTime = it.getLong("creation_time"),
+                expirationTime = it.getLong("expiration_time"),
+                bankCustomer = it.getLong("bank_customer"),
+                scope = it.getString("scope").run {
+                    if (this == TokenScope.readwrite.name) return@run 
TokenScope.readwrite
+                    if (this == TokenScope.readonly.name) return@run 
TokenScope.readonly
+                    else throw internalServerError("Wrong token scope found in 
the database: $this")
+                }
+            )
+        }
+    }
+
     // BANK ACCOUNTS
     // Returns false on conflicts.
     fun bankAccountCreate(bankAccount: BankAccount): Boolean {
         reconnect()
+        // FIXME: likely to be changed to only do internal_payto_uri
         val stmt = prepare("""
             INSERT INTO bank_accounts
-              (iban
-              ,bic
-              ,bank_account_label
+              (internal_payto_uri
               ,owning_customer_id
               ,is_public
-              ,last_nexus_fetch_row_id
+              ,is_taler_exchange
+              ,max_debt
               )
-            VALUES (?, ?, ?, ?, ?, ?)
+            VALUES (?, ?, ?, ?, (?, ?)::taler_amount)
         """)
-        stmt.setString(1, bankAccount.iban)
-        stmt.setString(2, bankAccount.bic)
-        stmt.setString(3, bankAccount.bankAccountLabel)
-        stmt.setLong(4, bankAccount.owningCustomerId)
-        stmt.setBoolean(5, bankAccount.isPublic)
-        stmt.setLong(6, bankAccount.lastNexusFetchRowId)
+        stmt.setString(1, bankAccount.internalPaytoUri)
+        stmt.setLong(2, bankAccount.owningCustomerId)
+        stmt.setBoolean(3, bankAccount.isPublic)
+        stmt.setBoolean(4, bankAccount.isTalerExchange)
+        stmt.setLong(5, bankAccount.maxDebt.value)
+        stmt.setInt(6, bankAccount.maxDebt.frac)
         // using the default zero value for the balance.
         return myExecute(stmt)
     }
 
     fun bankAccountSetMaxDebt(
-        bankAccountLabel: String,
+        owningCustomerId: Long,
         maxDebt: TalerAmount
     ): Boolean {
         reconnect()
         val stmt = prepare("""
            UPDATE bank_accounts
            SET max_debt=(?,?)::taler_amount
-           WHERE bank_account_label=?
+           WHERE owning_customer_id=?
         """)
         stmt.setLong(1, maxDebt.value)
         stmt.setInt(2, maxDebt.frac)
-        stmt.setString(3, bankAccountLabel)
+        stmt.setLong(3, owningCustomerId)
         return myExecute(stmt)
     }
 
-    fun bankAccountGetFromLabel(bankAccountLabel: String): BankAccount? {
+    fun bankAccountGetFromOwnerId(ownerId: Long): BankAccount? {
         reconnect()
         val stmt = prepare("""
             SELECT
-             iban
-             ,bic
+             internal_payto_uri
              ,owning_customer_id
              ,is_public
+             ,is_taler_exchange
              ,last_nexus_fetch_row_id
-             ,(balance).val AS balance_value
+             ,(balance).val AS balance_val
              ,(balance).frac AS balance_frac
              ,has_debt
+             ,(max_debt).val AS max_debt_val
+             ,(max_debt).frac AS max_debt_frac
             FROM bank_accounts
-            WHERE bank_account_label=?
+            WHERE owning_customer_id=?
         """)
-        stmt.setString(1, bankAccountLabel)
+        stmt.setLong(1, ownerId)
 
         val rs = stmt.executeQuery()
         rs.use {
             if (!it.next()) return null
             return BankAccount(
-                iban = it.getString("iban"),
-                bic = it.getString("bic"),
+                internalPaytoUri = it.getString("internal_payto_uri"),
                 balance = TalerAmount(
-                    it.getLong("balance_value"),
+                    it.getLong("balance_val"),
                     it.getInt("balance_frac")
                 ),
-                bankAccountLabel = bankAccountLabel,
                 lastNexusFetchRowId = it.getLong("last_nexus_fetch_row_id"),
                 owningCustomerId = it.getLong("owning_customer_id"),
-                hasDebt = it.getBoolean("has_debt")
+                hasDebt = it.getBoolean("has_debt"),
+                isTalerExchange = it.getBoolean("is_taler_exchange"),
+                maxDebt = TalerAmount(
+                    value = it.getLong("max_debt_val"),
+                    frac = it.getInt("max_debt_frac")
+                )
             )
         }
     }
@@ -355,11 +512,9 @@ class Database(private val dbConfig: String) {
         reconnect()
         val stmt = prepare("""
             SELECT 
-              creditor_iban
-              ,creditor_bic
+              creditor_payto_uri
               ,creditor_name
-              ,debtor_iban
-              ,debtor_bic
+              ,debtor_payto_uri
               ,debtor_name
               ,subject
               ,(amount).val AS amount_val
@@ -386,11 +541,9 @@ class Database(private val dbConfig: String) {
             do {
                 ret.add(
                     BankAccountTransaction(
-                        creditorIban = it.getString("creditor_iban"),
-                        creditorBic = it.getString("creditor_bic"),
+                        creditorPaytoUri = it.getString("creditor_payto_uri"),
                         creditorName = it.getString("creditor_name"),
-                        debtorIban = it.getString("debtor_iban"),
-                        debtorBic = it.getString("debtor_bic"),
+                        debtorPaytoUri = it.getString("debtor_payto_uri"),
                         debtorName = it.getString("debtor_name"),
                         amount = TalerAmount(
                             it.getLong("amount_val"),
@@ -409,7 +562,8 @@ class Database(private val dbConfig: String) {
                         paymentInformationId = 
it.getString("payment_information_id"),
                         subject = it.getString("subject"),
                         transactionDate = it.getLong("transaction_date")
-                ))
+                )
+                )
             } while (it.next())
             return ret
         }
@@ -516,7 +670,7 @@ class Database(private val dbConfig: String) {
               ,tan_channel
               ,tan_code
               ,bank_account
-              ,cashout_address
+              ,credit_payto_uri
               ,cashout_currency
            )
             VALUES (
@@ -552,7 +706,7 @@ class Database(private val dbConfig: String) {
         stmt.setString(14, op.tanChannel.name)
         stmt.setString(15, op.tanCode)
         stmt.setLong(16, op.bankAccount)
-        stmt.setString(17, op.cashoutAddress)
+        stmt.setString(17, op.credit_payto_uri)
         stmt.setString(18, op.cashoutCurrency)
         return myExecute(stmt)
     }
@@ -610,7 +764,7 @@ class Database(private val dbConfig: String) {
              ,tan_channel
              ,tan_code
              ,bank_account
-             ,cashout_address
+             ,credit_payto_uri
              ,cashout_currency
             ,tan_confirmation_time
             ,local_transaction
@@ -635,7 +789,7 @@ class Database(private val dbConfig: String) {
                     value = it.getLong("buy_in_fee_val"),
                     frac = it.getInt("buy_in_fee_frac")
                 ),
-                cashoutAddress = it.getString("cashout_address"),
+                credit_payto_uri = it.getString("credit_payto_uri"),
                 cashoutCurrency = it.getString("cashout_currency"),
                 cashoutUuid = opUuid,
                 creationTime = it.getLong("creation_time"),
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/EbicsProtocolBackend.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/EbicsProtocolBackend.kt
deleted file mode 100644
index a2a6deb9..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/EbicsProtocolBackend.kt
+++ /dev/null
@@ -1,1436 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2019 Stanisci and Dold.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING.  If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-
-package tech.libeufin.bank
-
-import io.ktor.server.application.*
-import io.ktor.http.ContentType
-import io.ktor.http.HttpStatusCode
-import io.ktor.server.request.*
-import io.ktor.server.response.respond
-import io.ktor.server.response.respondText
-import io.ktor.util.AttributeKey
-import io.ktor.util.date.*
-import org.apache.xml.security.binding.xmldsig.RSAKeyValueType
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.statements.api.ExposedBlob
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.w3c.dom.Document
-import tech.libeufin.util.*
-import tech.libeufin.util.XMLUtil.Companion.signEbicsResponse
-import tech.libeufin.util.ebics_h004.*
-import tech.libeufin.util.ebics_hev.HEVResponse
-import tech.libeufin.util.ebics_hev.SystemReturnCodeType
-import tech.libeufin.util.ebics_s001.SignatureTypes
-import tech.libeufin.util.ebics_s001.UserSignatureData
-import java.math.BigDecimal
-import java.security.interfaces.RSAPrivateCrtKey
-import java.security.interfaces.RSAPublicKey
-import java.sql.Connection
-import java.util.*
-import java.util.zip.DeflaterInputStream
-import java.util.zip.InflaterInputStream
-
-val EbicsHostIdAttribute = AttributeKey<String>("RequestedEbicsHostID")
-
-data class PainParseResult(
-    val creditorIban: String,
-    val creditorName: String,
-    val creditorBic: String?,
-    val debtorIban: String,
-    val debtorName: String,
-    val debtorBic: String?,
-    val subject: String,
-    val amount: String,
-    val currency: String,
-    val pmtInfId: String,
-    val endToEndId: String,
-    val msgId: String
-)
-
-open class EbicsRequestError(
-    val errorText: String,
-    val errorCode: String
-) : Exception("$errorText (EBICS error code: $errorCode)")
-
-class EbicsNoDownloadDataAvailable(reason: String? = null) : EbicsRequestError(
-    "[EBICS_NO_DOWNLOAD_DATA_AVAILABLE]" + if (reason != null) " $reason" else 
"",
-    "090005"
-)
-
-class EbicsInvalidRequestError : EbicsRequestError(
-    "[EBICS_INVALID_REQUEST] Invalid request",
-    "060102"
-)
-class EbicsAccountAuthorisationFailed(reason: String) : EbicsRequestError(
-    "[EBICS_ACCOUNT_AUTHORISATION_FAILED] $reason",
-    "091302"
-)
-
-/**
- * This error is thrown whenever the Subscriber's state is not suitable
- * for the requested action.  For example, the subscriber sends a EbicsRequest
- * message without having first uploaded their keys (#5973).
- */
-class EbicsSubscriberStateError : EbicsRequestError(
-    "[EBICS_INVALID_USER_OR_USER_STATE] Subscriber unknown or subscriber state 
inadmissible",
-    "091002"
-)
-// hint should mention at least the userID
-class EbicsUserUnknown(hint: String) : EbicsRequestError(
-    "[EBICS_USER_UNKNOWN] $hint",
-    "091003"
-)
-
-class EbicsOrderParamsIgnored(hint: String) : EbicsRequestError(
-    "[EBICS_ORDER_PARAMS_IGNORED] $hint",
-    "031001"
-)
-
-
-open class EbicsKeyManagementError(private val errorText: String, private val 
errorCode: String) :
-    Exception("EBICS key management error: $errorText ($errorCode)")
-
-private class EbicsInvalidXmlError : EbicsKeyManagementError(
-    "[EBICS_INVALID_XML]",
-    "091010"
-)
-
-private class EbicsUnsupportedOrderType : EbicsRequestError(
-    "[EBICS_UNSUPPORTED_ORDER_TYPE] Order type not supported",
-    "091005"
-)
-
-/**
- * Used here also for "Internal server error".  For example, when the
- * sandbox itself generates a invalid XML response.
- */
-class EbicsProcessingError(detail: String?) : EbicsRequestError(
-    // a missing detail is already the bank's fault.
-    "[EBICS_PROCESSING_ERROR] " + (detail ?: "bank internal error"),
-    "091116"
-)
-
-class EbicsAmountCheckError(detail: String): EbicsRequestError(
-    "[EBICS_AMOUNT_CHECK_FAILED] $detail",
-    "091303"
-)
-
-suspend fun respondEbicsTransfer(
-    call: ApplicationCall,
-    errorText: String,
-    errorCode: String
-) {
-    /**
-     * Because this handler runs for any error, it could
-     * handle the case where the Ebics host ID is unknown due
-     * to an invalid request.  Recall: Sandbox is multi-host, and
-     * which Ebics host was requested belongs to the request document.
-     *
-     * Therefore, because any Ebics response
-     * should speak for one Ebics host, we can't respond any Ebics
-     * type when the Ebics host ID remains unknown due to invalid
-     * request.  Instead, we'll respond plain text:
-     */
-    if (!call.attributes.contains(EbicsHostIdAttribute)) {
-        call.respondText("Invalid document.", status = 
HttpStatusCode.BadRequest)
-        return
-    }
-    val resp = EbicsResponse.createForUploadWithError(
-        errorText,
-        errorCode,
-        // For now, phase gets hard-coded as TRANSFER,
-        // because errors during initialization should have
-        // already been caught by the chunking logic.
-        EbicsTypes.TransactionPhaseType.TRANSFER
-    )
-    val hostAuthPriv = transaction {
-        val host = EbicsHostEntity.find {
-            EbicsHostsTable.hostID.upperCase() eq 
call.attributes[EbicsHostIdAttribute]
-                .uppercase()
-        }.firstOrNull() ?: throw SandboxError(
-            HttpStatusCode.InternalServerError,
-            "Requested Ebics host ID 
(${call.attributes[EbicsHostIdAttribute]}) not found."
-        )
-        CryptoUtil.loadRsaPrivateKey(host.authenticationPrivateKey.bytes)
-    }
-    call.respondText(
-        signEbicsResponse(resp, hostAuthPriv),
-        ContentType.Application.Xml,
-        HttpStatusCode.OK
-    )
-}
-
-private suspend fun ApplicationCall.respondEbicsKeyManagement(
-    errorText: String,
-    errorCode: String,
-    bankReturnCode: String,
-    dataTransfer: CryptoUtil.EncryptionResult? = null,
-    orderId: String? = null
-) {
-    val responseXml = EbicsKeyManagementResponse().apply {
-        version = "H004"
-        header = EbicsKeyManagementResponse.Header().apply {
-            authenticate = true
-            mutable = EbicsKeyManagementResponse.MutableHeaderType().apply {
-                reportText = errorText
-                returnCode = errorCode
-                if (orderId != null) {
-                    this.orderID = orderId
-                }
-            }
-            _static = EbicsKeyManagementResponse.EmptyStaticHeader()
-        }
-        body = EbicsKeyManagementResponse.Body().apply {
-            this.returnCode = EbicsKeyManagementResponse.ReturnCode().apply {
-                this.authenticate = true
-                this.value = bankReturnCode
-            }
-            if (dataTransfer != null) {
-                this.dataTransfer = 
EbicsKeyManagementResponse.DataTransfer().apply {
-                    this.dataEncryptionInfo = 
EbicsTypes.DataEncryptionInfo().apply {
-                        this.authenticate = true
-                        this.transactionKey = 
dataTransfer.encryptedTransactionKey
-                        this.encryptionPubKeyDigest = 
EbicsTypes.PubKeyDigest().apply {
-                            this.algorithm = 
"http://www.w3.org/2001/04/xmlenc#sha256";
-                            this.version = "E002"
-                            this.value = dataTransfer.pubKeyDigest
-                        }
-                    }
-                    this.orderData = 
EbicsKeyManagementResponse.OrderData().apply {
-                        this.value = 
Base64.getEncoder().encodeToString(dataTransfer.encryptedData)
-                    }
-                }
-            }
-        }
-    }
-    val text = XMLUtil.convertJaxbToString(responseXml)
-    // logger.info("responding with:\n${text}")
-    if (!XMLUtil.validateFromString(text)) throw SandboxError(
-        HttpStatusCode.InternalServerError,
-        "Outgoint EBICS key management response is invalid"
-    )
-    respondText(text, ContentType.Application.Xml, HttpStatusCode.OK)
-}
-
-fun <T> expectNonNull(x: T?): T {
-    if (x == null) {
-        throw EbicsProtocolError(HttpStatusCode.BadRequest, "expected non-null 
value")
-    }
-    return x;
-}
-
-private fun getRelatedParty(branch: XmlElementBuilder, payment: 
XLibeufinBankTransaction) {
-    val otherParty = object {
-        var ibanPath = "CdtrAcct/Id/IBAN"
-        var namePath = "Cdtr/Nm"
-        var iban = payment.creditorIban
-        var name = payment.creditorName
-        var bicPath = "CdtrAgt/FinInstnId/BIC"
-        var bic = payment.creditorBic
-    }
-    if (payment.direction == XLibeufinBankDirection.CREDIT) {
-        otherParty.iban = payment.debtorIban
-        otherParty.ibanPath = "DbtrAcct/Id/IBAN"
-        otherParty.namePath = "Dbtr/Nm"
-        otherParty.name = payment.debtorName
-        otherParty.bic = payment.debtorBic
-        otherParty.bicPath = "DbtrAgt/FinInstnId/BIC"
-    }
-    branch.element("RltdPties") {
-        element(otherParty.namePath) {
-            text(otherParty.name)
-        }
-        element(otherParty.ibanPath) {
-            text(otherParty.iban)
-        }
-    }
-    val otherPartyBic = otherParty.bic
-    if (otherPartyBic != null) {
-        branch.element("RltdAgts") {
-            element(otherParty.bicPath) {
-                text(otherPartyBic)
-            }
-        }
-    }
-}
-
-// This should fix #6269.
-private fun getCreditDebitInd(balance: BigDecimal): String {
-    if (balance < BigDecimal.ZERO) return "DBIT"
-    return "CRDT"
-}
-
-fun buildCamtString(
-    type: Int,
-    subscriberIban: String,
-    history: MutableList<XLibeufinBankTransaction>,
-    currency: String
-): SandboxCamt {
-    /**
-     * ID types required:
-     *
-     * - Message Id
-     * - Statement / Report Id
-     * - Electronic sequence number
-     * - Legal sequence number
-     * - Entry Id by the Servicer
-     * - Payment information Id
-     * - Proprietary code of the bank transaction
-     * - Id of the servicer (Issuer and Code)
-     */
-    val camtCreationTime = getSystemTimeNow() // FIXME: should this be the 
payment time?
-    val dashedDate = camtCreationTime.toDashedDate()
-    val zonedDateTime = camtCreationTime.toZonedString()
-    val creationTimeMillis = camtCreationTime.toInstant().toEpochMilli()
-    val messageId = "sandbox-${creationTimeMillis / 
1000}-${getRandomString(10)}"
-
-    val camtMessage = constructXml(indent = true) {
-        root("Document") {
-            attribute("xmlns", 
"urn:iso:std:iso:20022:tech:xsd:camt.0${type}.001.02")
-            attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance";)
-            attribute(
-                "xsi:schemaLocation",
-                "urn:iso:std:iso:20022:tech:xsd:camt.0${type}.001.02 
camt.0${type}.001.02.xsd"
-            )
-            element(if (type == 53) "BkToCstmrStmt" else "BkToCstmrAcctRpt") {
-                element("GrpHdr") {
-                    element("MsgId") {
-                        text(messageId)
-                    }
-                    element("CreDtTm") {
-                        text(zonedDateTime)
-                    }
-                }
-                element(if (type == 52) "Rpt" else "Stmt") {
-                    element("Id") {
-                        text("0")
-                    }
-                    element("ElctrncSeqNb") {
-                        text("0")
-                    }
-                    element("LglSeqNb") {
-                        text("0")
-                    }
-                    element("CreDtTm") {
-                        text(zonedDateTime)
-                    }
-                    element("Acct") {
-                        // mandatory account identifier
-                        element("Id/IBAN") {
-                            text(subscriberIban)
-                        }
-                        element("Ccy") {
-                            text(currency)
-                        }
-                        element("Ownr/Nm") {
-                            text("Debitor/Owner Name")
-                        }
-                        element("Svcr/FinInstnId") {
-                            element("Nm") {
-                                text("Libeufin Bank")
-                            }
-                            element("Othr") {
-                                element("Id") {
-                                    text("0")
-                                }
-                                element("Issr") {
-                                    text("XY")
-                                }
-                            }
-                        }
-                    }
-                    history.forEach {
-                        this.element("Ntry") {
-                            element("Amt") {
-                                attribute("Ccy", it.currency)
-                                text(it.amount)
-                            }
-                            element("CdtDbtInd") {
-                                text(
-                                    if (subscriberIban.equals(it.creditorIban))
-                                        "CRDT" else "DBIT"
-                                )
-                            }
-                            element("Sts") {
-                                /* Status of the entry (see 2.4.2.15.5 from 
the ISO20022 reference document.)
-                                    * From the original text:
-                                    * "Status of an entry on the books of the 
account servicer" */
-                                text("BOOK")
-                            }
-                            element("BookgDt/Dt") {
-                                text(dashedDate)
-                            } // date of the booking
-                            element("ValDt/Dt") {
-                                text(dashedDate)
-                            } // date of assets' actual (un)availability
-                            element("AcctSvcrRef") {
-                                text(it.uid)
-                            }
-                            element("BkTxCd") {
-                                /*  "Set of elements used to fully identify 
the type of underlying
-                                 *   transaction resulting in an entry".  */
-                                element("Domn") {
-                                    element("Cd") {
-                                        text("PMNT")
-                                    }
-                                    element("Fmly") {
-                                        element("Cd") {
-                                            text("ICDT")
-                                        }
-                                        element("SubFmlyCd") {
-                                            text("ESCT")
-                                        }
-                                    }
-                                }
-                                element("Prtry") {
-                                    element("Cd") {
-                                        text("0")
-                                    }
-                                    element("Issr") {
-                                        text("XY")
-                                    }
-                                }
-                            }
-                            element("NtryDtls/TxDtls") {
-                                element("Refs") {
-                                    element("MsgId") {
-                                        text(it.msgId ?: "NOTPROVIDED")
-                                    }
-                                    element("PmtInfId") {
-                                        text(it.pmtInfId ?: "NOTPROVIDED")
-                                    }
-                                    element("EndToEndId") {
-                                        text(it.endToEndId ?: "NOTPROVIDED")
-                                    }
-                                }
-                                element("AmtDtls/TxAmt/Amt") {
-                                    attribute("Ccy", currency)
-                                    text(it.amount)
-                                }
-                                element("BkTxCd") {
-                                    element("Domn") {
-                                        element("Cd") {
-                                            text("PMNT")
-                                        }
-                                        element("Fmly") {
-                                            element("Cd") {
-                                                text("ICDT")
-                                            }
-                                            element("SubFmlyCd") {
-                                                text("ESCT")
-                                            }
-                                        }
-                                    }
-                                    element("Prtry") {
-                                        element("Cd") {
-                                            text("0")
-                                        }
-                                        element("Issr") {
-                                            text("XY")
-                                        }
-                                    }
-                                }
-                                getRelatedParty(this, it)
-                                element("RmtInf/Ustrd") {
-                                    text(it.subject)
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-    return SandboxCamt(
-        camtMessage = camtMessage,
-        messageId = messageId,
-        creationTime = creationTimeMillis
-    )
-}
-
-/**
- * Builds CAMT response.
- *
- * @param type 52 or 53.
- */
-private fun constructCamtResponse(
-    type: Int,
-    subscriber: EbicsSubscriberEntity,
-    dateRange: Pair<Long, Long>?
-): List<String> {
-    if (type != 53 && type != 52) throw EbicsUnsupportedOrderType()
-    val bankAccount = getBankAccountFromSubscriber(subscriber)
-    val history = mutableListOf<XLibeufinBankTransaction>()
-    if (type == 52) {
-        if (dateRange != null) {
-            logger.debug("Finding date-ranged transactions for account: 
${bankAccount.label}, range: ${dateRange.first}, ${dateRange.second}")
-            transaction {
-                BankAccountTransactionEntity.find {
-                    BankAccountTransactionsTable.account eq bankAccount.id and
-                            BankAccountTransactionsTable.date.between(
-                                dateRange.first, dateRange.second
-                            )
-                }.forEach { 
history.add(getHistoryElementFromTransactionRow(it)) }
-            }
-        } else
-            transaction {
-                BankAccountFreshTransactionEntity.all().forEach {
-                    if (it.transactionRef.account.label == bankAccount.label) {
-                        history.add(getHistoryElementFromTransactionRow(it))
-                    }
-                }
-            }
-        if (history.size == 0) throw EbicsNoDownloadDataAvailable()
-        val camtData = buildCamtString(
-            type,
-            bankAccount.iban,
-            history,
-            bankAccount.demoBank.config.currency
-        )
-        val paymentsList: String = if (logger.isDebugEnabled) {
-            var ret = " It includes the payments:"
-            for (p in history) ret += "\n- ${p.subject}"
-            ret
-        } else ""
-        logger.debug("camt.052 document '${camtData.messageId}' 
generated.$paymentsList")
-        return listOf(camtData.camtMessage)
-    } // end of C52 case.
-    val ret = mutableListOf<String>()
-    /**
-     * Retrieve all the records whose creation date lies into the
-     * time range given in the function parameters.
-     */
-    if (dateRange != null) {
-        logger.debug("Serving C53 with date range: $dateRange")
-        BankAccountStatementEntity.find {
-            BankAccountStatementsTable.creationTime.between(
-                dateRange.first,
-                dateRange.second) and(
-                    BankAccountStatementsTable.bankAccount eq bankAccount.id)
-        }.forEach {
-            logger.debug("Including Camt.053: ${it.statementId}")
-            ret.add(it.xmlMessage)
-        }
-    } else {
-        logger.debug("Serving C53 without date range.")
-        // No time range was given, hence pick the latest statement.
-        BankAccountStatementEntity.find {
-            BankAccountStatementsTable.bankAccount eq bankAccount.id
-        }.lastOrNull().apply {
-            if (this != null) {
-                logger.debug("Including Camt.053: ${this.statementId}")
-                ret.add(this.xmlMessage)
-            }
-        }
-    }
-    if (ret.size == 0) throw EbicsNoDownloadDataAvailable()
-    return ret
-}
-
-/**
- * TSD (test download) message.
- *
- * This is a non-standard EBICS order type use by LibEuFin to
- * test download transactions.
- *
- * In the future, additional parameters (size, chunking, inject fault for 
retry) might
- * be added to the order parameters.
- */
-private fun handleEbicsTSD(): ByteArray {
-    return "Hello World\n".repeat(1024).toByteArray()
-}
-
-private fun handleEbicsPTK(): ByteArray {
-    return "Hello I am a dummy PTK response.".toByteArray()
-}
-
-private fun parsePain001(paymentRequest: String): PainParseResult {
-    val painDoc = XMLUtil.parseStringIntoDom(paymentRequest)
-    return destructXml(painDoc) {
-        requireRootElement("Document") {
-            requireUniqueChildNamed("CstmrCdtTrfInitn") {
-                val msgId = requireUniqueChildNamed("GrpHdr") {
-                    requireUniqueChildNamed("MsgId") { 
focusElement.textContent }
-                }
-                requireUniqueChildNamed("PmtInf") {
-                    val debtorName = requireUniqueChildNamed("Dbtr"){
-                        requireUniqueChildNamed("Nm") {
-                            focusElement.textContent
-                        }
-                    }
-                    val debtorIban = requireUniqueChildNamed("DbtrAcct"){
-                        requireUniqueChildNamed("Id") {
-                            requireUniqueChildNamed("IBAN") {
-                                focusElement.textContent
-                            }
-                        }
-                    }
-                    val debtorBic = requireUniqueChildNamed("DbtrAgt"){
-                        requireUniqueChildNamed("FinInstnId") {
-                            requireUniqueChildNamed("BIC") {
-                                focusElement.textContent
-                            }
-                        }
-                    }
-                    val pmtInfId = requireUniqueChildNamed("PmtInfId") { 
focusElement.textContent }
-                    val txDetails = requireUniqueChildNamed("CdtTrfTxInf") {
-                        object {
-                            val creditorIban = 
requireUniqueChildNamed("CdtrAcct") {
-                                requireUniqueChildNamed("Id") {
-                                    requireUniqueChildNamed("IBAN") { 
focusElement.textContent }
-                                }
-                            }
-                            val creditorName = requireUniqueChildNamed("Cdtr") 
{
-                                requireUniqueChildNamed("Nm") {
-                                    focusElement.textContent
-                                }
-                            }
-                            val creditorBic = maybeUniqueChildNamed("CdtrAgt") 
{
-                                requireUniqueChildNamed("FinInstnId") {
-                                    requireUniqueChildNamed("BIC") {
-                                        focusElement.textContent
-                                    }
-                                }
-                            }
-                            val amt = requireUniqueChildNamed("Amt") {
-                                requireOnlyChild { focusElement }
-                            }
-                            val subject = requireUniqueChildNamed("RmtInf") {
-                                requireUniqueChildNamed("Ustrd") { 
focusElement.textContent }
-                            }
-                            val endToEndId = requireUniqueChildNamed("PmtId") {
-                                requireUniqueChildNamed("EndToEndId") { 
focusElement.textContent }
-                            }
-                        }
-                    }
-                    /**
-                     * NOTE: this check breaks the compatibility with pain.001,
-                     * because that allows up to 5 fractional digits.  For 
Taler
-                     * compatibility however, we enforce the max 2 fractional 
digits policy.
-                     */
-                    if (!validatePlainAmount(txDetails.amt.textContent)) {
-                        throw EbicsProcessingError(
-                            "Amount number malformed: 
${txDetails.amt.textContent}"
-                        )
-                    }
-                    PainParseResult(
-                        currency = txDetails.amt.getAttribute("Ccy"),
-                        amount = txDetails.amt.textContent,
-                        subject = txDetails.subject,
-                        debtorIban = debtorIban,
-                        debtorName = debtorName,
-                        debtorBic = debtorBic,
-                        creditorName = txDetails.creditorName,
-                        creditorIban = txDetails.creditorIban,
-                        creditorBic = txDetails.creditorBic,
-                        pmtInfId = pmtInfId,
-                        endToEndId = txDetails.endToEndId,
-                        msgId = msgId
-                    )
-                }
-            }
-        }
-    }
-}
-
-/**
- * Process a payment request in the pain.001 format.  Note:
- * the receiver IBAN is NOT checked to have one account at
- * the Sandbox.  That's because (1) it leaves open to send
- * payments outside of the running Sandbox and (2) may ease
- * tests where the preparation logic can skip creating also
- * the receiver account.  */
-private fun handleCct(
-    paymentRequest: String,
-    requestingSubscriber: EbicsSubscriberEntity
-) {
-    val parseResult = parsePain001(paymentRequest)
-    logger.debug("Handling Pain.001: ${parseResult.pmtInfId}, " +
-            "for payment: ${parseResult.subject}")
-    transaction(Connection.TRANSACTION_SERIALIZABLE, repetitionAttempts = 10) {
-        // Check that subscriber has a bank account
-        // and that they have rights over the debtor IBAN
-        if (requestingSubscriber.bankAccount == null) throw 
EbicsProcessingError(
-            "Subscriber '${requestingSubscriber.userId}' does not have a bank 
account."
-        )
-        if (requestingSubscriber.bankAccount!!.iban != parseResult.debtorIban) 
throw
-                EbicsAccountAuthorisationFailed(
-                    "Subscriber '${requestingSubscriber.userId}' does not have 
rights" +
-                            " over the debtor IBAN '${parseResult.debtorIban}'"
-                )
-        val maybeExist = BankAccountTransactionEntity.find {
-            BankAccountTransactionsTable.pmtInfId eq parseResult.pmtInfId
-        }.firstOrNull()
-        if (maybeExist != null) {
-            logger.info(
-                "Nexus submitted twice the Pain: ${maybeExist.pmtInfId}. Not 
taking any action." +
-                        "  Sandbox gave it this reference: 
${maybeExist.accountServicerReference}"
-            )
-            return@transaction
-        }
-        val bankAccount = getBankAccountFromIban(parseResult.debtorIban)
-        if (parseResult.currency != bankAccount.demoBank.config.currency) 
throw EbicsRequestError(
-            "[EBICS_PROCESSING_ERROR] Currency (${parseResult.currency}) not 
supported.",
-            "091116"
-        )
-        // Check for the debit case.
-        val maybeAmount = try {
-            BigDecimal(parseResult.amount)
-        } catch (e: Exception) {
-            logger.warn("Although PAIN validated, BigDecimal didn't parse its 
amount (${parseResult.amount})!")
-            throw EbicsProcessingError("The CCT request contains an invalid 
amount: ${parseResult.amount}")
-        }
-        if (maybeDebit(bankAccount.label, maybeAmount, 
bankAccount.demoBank.name))
-            throw EbicsAmountCheckError("The requested amount 
(${parseResult.amount}) would exceed the debit threshold")
-        logger.debug("Wire-transfer'ing endToEndId: ${parseResult.endToEndId}")
-        wireTransfer(
-            bankAccount.label,
-            getBankAccountFromIban(parseResult.creditorIban).label,
-            bankAccount.demoBank.name,
-            parseResult.subject,
-            "${parseResult.currency}:${parseResult.amount}",
-            endToEndId = parseResult.endToEndId
-        )
-    }
-}
-
-/**
- * This handler reports all the fresh transactions, belonging
- * to the querying subscriber.
- */
-private fun handleEbicsC52(requestContext: RequestContext): ByteArray {
-    val maybeDateRange = 
requestContext.requestObject.header.static.orderDetails?.orderParams
-    val dateRange: Pair<Long, Long>? = if (maybeDateRange is 
EbicsRequest.StandardOrderParams) {
-        val start: Long? = 
maybeDateRange.dateRange?.start?.toGregorianCalendar()?.timeInMillis
-        val end: Long? = 
maybeDateRange.dateRange?.end?.toGregorianCalendar()?.timeInMillis
-        Pair(start ?: 0L, end ?: Long.MAX_VALUE)
-    } else null
-    logger.debug("Date range: $dateRange")
-    val report = constructCamtResponse(
-        52,
-        requestContext.subscriber,
-        dateRange = dateRange
-    )
-    sandboxAssert(
-        report.size == 1,
-        "C52 response contains more than one Camt.052 document"
-    )
-    if (!XMLUtil.validateFromString(report[0])) {
-        logger.error("This document was generated invalid:\n${report[0]}")
-        throw EbicsProcessingError("One outgoing report was found invalid.")
-    }
-    return report.map { it.toByteArray() }.zip()
-}
-
-private fun handleEbicsC53(requestContext: RequestContext): ByteArray {
-    // Fetch date range.
-    val orderParams = 
requestContext.requestObject.header.static.orderDetails?.orderParams // as 
EbicsRequest.StandardOrderParams
-    val dateRange = if (orderParams != null) {
-        val standardOrderParams = orderParams as 
EbicsRequest.StandardOrderParams
-        val start = 
standardOrderParams.dateRange?.start?.toGregorianCalendar()?.timeInMillis
-        val end = 
standardOrderParams.dateRange?.end?.toGregorianCalendar()?.timeInMillis
-        if (start == null || end == null) {
-            // only accepting when both start/end are given.
-            null
-        } else {
-            Pair(start, end)
-        }
-    } else
-        null
-    /**
-     * By multiple statements, this function is responsible to return
-     * a list of Strings: one for each statement.
-     */
-    val camtStatements = constructCamtResponse(
-        53,
-        requestContext.subscriber,
-        dateRange
-    )
-    camtStatements.forEach {
-        if (!XMLUtil.validateFromString(it)) {
-            logger.error("This document was generated invalid:\n$it")
-            throw EbicsProcessingError("One outgoing statement was found 
invalid.")
-        }
-    }
-    return camtStatements.map { it.toByteArray() }.zip()
-}
-
-private suspend fun ApplicationCall.handleEbicsHia(header: 
EbicsUnsecuredRequest.Header, orderData: ByteArray) {
-    InflaterInputStream(orderData.inputStream()).use { it.readAllBytes() }
-    val keyObject = 
EbicsOrderUtil.decodeOrderDataXml<HIARequestOrderData>(orderData)
-    val encPubXml = keyObject.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue
-    val authPubXml = keyObject.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue
-    val encPub = CryptoUtil.loadRsaPublicKeyFromComponents(encPubXml.modulus, 
encPubXml.exponent)
-    val authPub = 
CryptoUtil.loadRsaPublicKeyFromComponents(authPubXml.modulus, 
authPubXml.exponent)
-
-    val ok = transaction {
-        val ebicsSubscriber = findEbicsSubscriber(header.static.partnerID, 
header.static.userID, header.static.systemID)
-        if (ebicsSubscriber == null) {
-            logger.warn("ebics subscriber not found")
-            throw EbicsInvalidRequestError()
-        }
-        when (ebicsSubscriber.state) {
-            SubscriberState.NEW -> {}
-            SubscriberState.PARTIALLY_INITIALIZED_INI -> {}
-            SubscriberState.PARTIALLY_INITIALIZED_HIA, 
SubscriberState.INITIALIZED, SubscriberState.READY -> {
-                return@transaction false
-            }
-        }
-
-        ebicsSubscriber.authenticationKey = EbicsSubscriberPublicKeyEntity.new 
{
-            this.rsaPublicKey = ExposedBlob(authPub.encoded)
-            state = KeyState.NEW
-        }
-        ebicsSubscriber.encryptionKey = EbicsSubscriberPublicKeyEntity.new {
-            this.rsaPublicKey = ExposedBlob(encPub.encoded)
-            state = KeyState.NEW
-        }
-        ebicsSubscriber.state = when (ebicsSubscriber.state) {
-            SubscriberState.NEW -> SubscriberState.PARTIALLY_INITIALIZED_HIA
-            SubscriberState.PARTIALLY_INITIALIZED_INI -> 
SubscriberState.INITIALIZED
-            else -> throw Exception("internal invariant failed")
-        }
-        return@transaction true
-    }
-    if (ok) {
-        respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000")
-    } else {
-        respondEbicsKeyManagement("[EBICS_INVALID_USER_OR_USER_STATE]", 
"091002", "000000")
-    }
-}
-
-private suspend fun ApplicationCall.handleEbicsIni(header: 
EbicsUnsecuredRequest.Header, orderData: ByteArray) {
-    InflaterInputStream(orderData.inputStream()).use { it.readAllBytes() }
-    val keyObject = 
EbicsOrderUtil.decodeOrderDataXml<SignatureTypes.SignaturePubKeyOrderData>(orderData)
-    val sigPubXml = keyObject.signaturePubKeyInfo.pubKeyValue.rsaKeyValue
-    val sigPub = CryptoUtil.loadRsaPublicKeyFromComponents(sigPubXml.modulus, 
sigPubXml.exponent)
-
-    val ok = transaction {
-        val ebicsSubscriber =
-            findEbicsSubscriber(header.static.partnerID, header.static.userID, 
header.static.systemID)
-        if (ebicsSubscriber == null) {
-            logger.warn("ebics subscriber, 
${dumpEbicsSubscriber(header.static)}, not found")
-            throw EbicsUserUnknown(dumpEbicsSubscriber(header.static))
-        }
-        when (ebicsSubscriber.state) {
-            SubscriberState.NEW -> {}
-            SubscriberState.PARTIALLY_INITIALIZED_HIA -> {}
-            SubscriberState.PARTIALLY_INITIALIZED_INI, 
SubscriberState.INITIALIZED, SubscriberState.READY -> {
-                return@transaction false
-            }
-        }
-        ebicsSubscriber.signatureKey = EbicsSubscriberPublicKeyEntity.new {
-            this.rsaPublicKey = ExposedBlob(sigPub.encoded)
-            state = KeyState.NEW
-        }
-        ebicsSubscriber.state = when (ebicsSubscriber.state) {
-            SubscriberState.NEW -> SubscriberState.PARTIALLY_INITIALIZED_INI
-            SubscriberState.PARTIALLY_INITIALIZED_HIA -> 
SubscriberState.INITIALIZED
-            else -> throw Error("internal invariant failed")
-        }
-        return@transaction true
-    }
-    logger.info("Signature key inserted in database _and_ subscriber state 
changed accordingly")
-    if (ok) {
-        respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000")
-    } else {
-        respondEbicsKeyManagement("[EBICS_INVALID_USER_OR_USER_STATE]", 
"091002", "000000")
-    }
-}
-
-private suspend fun ApplicationCall.handleEbicsHpb(
-    ebicsHostInfo: EbicsHostPublicInfo,
-    requestDocument: Document,
-    header: EbicsNpkdRequest.Header
-) {
-    val subscriberKeys = transaction {
-        val ebicsSubscriber =
-            findEbicsSubscriber(header.static.partnerID, header.static.userID, 
header.static.systemID)
-        if (ebicsSubscriber == null) {
-            throw EbicsInvalidRequestError()
-        }
-        if (ebicsSubscriber.state != SubscriberState.INITIALIZED) {
-            throw EbicsSubscriberStateError()
-        }
-        val authPubBlob = ebicsSubscriber.authenticationKey!!.rsaPublicKey
-        val encPubBlob = ebicsSubscriber.encryptionKey!!.rsaPublicKey
-        val sigPubBlob = ebicsSubscriber.signatureKey!!.rsaPublicKey
-        SubscriberKeys(
-            CryptoUtil.loadRsaPublicKey(authPubBlob.bytes),
-            CryptoUtil.loadRsaPublicKey(encPubBlob.bytes),
-            CryptoUtil.loadRsaPublicKey(sigPubBlob.bytes)
-        )
-    }
-    val validationResult =
-        XMLUtil.verifyEbicsDocument(requestDocument, 
subscriberKeys.authenticationPublicKey)
-    if (!validationResult) {
-        throw EbicsKeyManagementError("invalid signature", "90000")
-    }
-    val hpbRespondeData = HPBResponseOrderData().apply {
-        this.authenticationPubKeyInfo = 
EbicsTypes.AuthenticationPubKeyInfoType().apply {
-            this.authenticationVersion = "X002"
-            this.pubKeyValue = EbicsTypes.PubKeyValueType().apply {
-                this.rsaKeyValue = RSAKeyValueType().apply {
-                    this.exponent = 
ebicsHostInfo.authenticationPublicKey.publicExponent.toByteArray()
-                    this.modulus = 
ebicsHostInfo.authenticationPublicKey.modulus.toByteArray()
-                }
-            }
-        }
-        this.encryptionPubKeyInfo = 
EbicsTypes.EncryptionPubKeyInfoType().apply {
-            this.encryptionVersion = "E002"
-            this.pubKeyValue = EbicsTypes.PubKeyValueType().apply {
-                this.rsaKeyValue = RSAKeyValueType().apply {
-                    this.exponent = 
ebicsHostInfo.encryptionPublicKey.publicExponent.toByteArray()
-                    this.modulus = 
ebicsHostInfo.encryptionPublicKey.modulus.toByteArray()
-                }
-            }
-        }
-        this.hostID = ebicsHostInfo.hostID
-    }
-    val compressedOrderData = 
EbicsOrderUtil.encodeOrderDataXml(hpbRespondeData)
-    val encryptionResult = CryptoUtil.encryptEbicsE002(compressedOrderData, 
subscriberKeys.encryptionPublicKey)
-    respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000", 
encryptionResult, "OR01")
-}
-
-/**
- * Find the ebics host corresponding to the one specified in the header.
- */
-private fun ensureEbicsHost(requestHostID: String): EbicsHostPublicInfo {
-    return transaction {
-        val ebicsHost =
-            EbicsHostEntity.find { EbicsHostsTable.hostID.upperCase() eq 
requestHostID.uppercase(Locale.getDefault()) }.firstOrNull()
-        if (ebicsHost == null) {
-            logger.warn("client requested unknown HostID ${requestHostID}")
-            throw EbicsKeyManagementError("[EBICS_INVALID_HOST_ID]", "091011")
-        }
-        val encryptionPrivateKey = 
CryptoUtil.loadRsaPrivateKey(ebicsHost.encryptionPrivateKey.bytes)
-        val authenticationPrivateKey = 
CryptoUtil.loadRsaPrivateKey(ebicsHost.authenticationPrivateKey.bytes)
-        EbicsHostPublicInfo(
-            requestHostID,
-            CryptoUtil.getRsaPublicFromPrivate(encryptionPrivateKey),
-            CryptoUtil.getRsaPublicFromPrivate(authenticationPrivateKey)
-        )
-    }
-}
-fun receiveEbicsXmlInternal(xmlData: String): Document {
-    // logger.debug("Data received: $xmlData")
-    val requestDocument: Document = XMLUtil.parseStringIntoDom(xmlData)
-    if (!XMLUtil.validateFromDom(requestDocument)) {
-        println("Problematic document was: $requestDocument")
-        throw EbicsInvalidXmlError()
-    }
-    return requestDocument
-}
-
-private fun makePartnerInfo(subscriber: EbicsSubscriberEntity): 
EbicsTypes.PartnerInfo {
-    val bankAccount = getBankAccountFromSubscriber(subscriber)
-    val customerProfile = getCustomer(bankAccount.label)
-    return EbicsTypes.PartnerInfo().apply {
-        this.accountInfoList = listOf(
-            EbicsTypes.AccountInfo().apply {
-                this.id = bankAccount.label
-                this.accountHolder = customerProfile.name ?: "Never Given"
-                this.accountNumberList = listOf(
-                    EbicsTypes.GeneralAccountNumber().apply {
-                        this.international = true
-                        this.value = bankAccount.iban
-                    }
-                )
-                this.currency = bankAccount.demoBank.config.currency
-                this.description = "Ordinary Bank Account"
-                this.bankCodeList = listOf(
-                    EbicsTypes.GeneralBankCode().apply {
-                        this.international = true
-                        this.value = bankAccount.bic
-                    }
-                )
-            }
-        )
-        this.addressInfo = EbicsTypes.AddressInfo().apply {
-            this.name = "Address Info Object"
-        }
-        this.bankInfo = EbicsTypes.BankInfo().apply {
-            this.hostID = subscriber.hostId
-        }
-        this.orderInfoList = listOf(
-            EbicsTypes.AuthOrderInfoType().apply {
-                this.description = "Transactions statement"
-                this.orderType = "C53"
-                this.transferType = "Download"
-            },
-            EbicsTypes.AuthOrderInfoType().apply {
-                this.description = "Transactions report"
-                this.orderType = "C52"
-                this.transferType = "Download"
-            },
-            EbicsTypes.AuthOrderInfoType().apply {
-                this.description = "Payment initiation (ZIPped payload)"
-                this.orderType = "CCC"
-                this.transferType = "Upload"
-            },
-            EbicsTypes.AuthOrderInfoType().apply {
-                this.description = "Payment initiation (plain text payload)"
-                this.orderType = "CCT"
-                this.transferType = "Upload"
-            },
-            EbicsTypes.AuthOrderInfoType().apply {
-                this.description = "vmk"
-                this.orderType = "VMK"
-                this.transferType = "Download"
-            },
-            EbicsTypes.AuthOrderInfoType().apply {
-                this.description = "sta"
-                this.orderType = "STA"
-                this.transferType = "Download"
-            }
-        )
-    }
-}
-
-private fun handleEbicsHtd(requestContext: RequestContext): ByteArray {
-    val htd = HTDResponseOrderData().apply {
-        this.partnerInfo = makePartnerInfo(requestContext.subscriber)
-        this.userInfo = EbicsTypes.UserInfo().apply {
-            this.name = "Some User"
-            this.userID = EbicsTypes.UserIDType().apply {
-                this.status = 5
-                this.value = requestContext.subscriber.userId
-            }
-            this.permissionList = listOf(
-                EbicsTypes.UserPermission().apply {
-                    this.orderTypes = "C53 C52 CCC VMK STA"
-                }
-            )
-        }
-    }
-    val str = XMLUtil.convertJaxbToString(htd)
-    return str.toByteArray()
-}
-
-private fun handleEbicsHkd(requestContext: RequestContext): ByteArray {
-    val hkd = HKDResponseOrderData().apply {
-        this.partnerInfo = makePartnerInfo(requestContext.subscriber)
-        this.userInfoList = listOf(
-            EbicsTypes.UserInfo().apply {
-                this.name = "Some User"
-                this.userID = EbicsTypes.UserIDType().apply {
-                    this.status = 1
-                    this.value = requestContext.subscriber.userId
-                }
-                this.permissionList = listOf(
-                    EbicsTypes.UserPermission().apply {
-                        this.orderTypes = "C54 C53 C52 CCC"
-                    }
-                )
-            })
-    }
-    val str = XMLUtil.convertJaxbToString(hkd)
-    return str.toByteArray()
-}
-
-private data class RequestContext(
-    val ebicsHost: EbicsHostEntity,
-    val subscriber: EbicsSubscriberEntity,
-    val clientEncPub: RSAPublicKey,
-    val clientAuthPub: RSAPublicKey,
-    val clientSigPub: RSAPublicKey,
-    val hostEncPriv: RSAPrivateCrtKey,
-    val hostAuthPriv: RSAPrivateCrtKey,
-    val requestObject: EbicsRequest,
-    val uploadTransaction: EbicsUploadTransactionEntity?,
-    val downloadTransaction: EbicsDownloadTransactionEntity?
-)
-
-/**
- * Get segmentation values and the EBICS transaction ID, before
- * handing the response to 'createForDownloadTransferPhase()'.
- */
-private fun handleEbicsDownloadTransactionTransfer(requestContext: 
RequestContext): EbicsResponse {
-    val segmentNumber =
-        requestContext.requestObject.header.mutable.segmentNumber?.value ?: 
throw EbicsInvalidRequestError()
-    val transactionID = 
requestContext.requestObject.header.static.transactionID ?: throw 
EbicsInvalidRequestError()
-    val downloadTransaction = requestContext.downloadTransaction ?: throw 
AssertionError()
-    return EbicsResponse.createForDownloadTransferPhase(
-        transactionID,
-        downloadTransaction.numSegments,
-        downloadTransaction.segmentSize,
-        downloadTransaction.encodedResponse,
-        segmentNumber.toInt()
-    )
-}
-
-private fun handleEbicsDownloadTransactionInitialization(requestContext: 
RequestContext): EbicsResponse {
-    val orderType =
-        requestContext.requestObject.header.static.orderDetails?.orderType ?: 
throw EbicsInvalidRequestError()
-    val nonce = requestContext.requestObject.header.static.nonce
-    val transactionID = EbicsOrderUtil.generateTransactionId()
-    logger.debug(
-        "Handling download initialization for order type $orderType, " +
-                "nonce: ${nonce?.toHexString() ?: "not given"}, " +
-                "transaction ID: $transactionID"
-    )
-    val response = when (orderType) {
-        "HTD" -> handleEbicsHtd(requestContext)
-        "HKD" -> handleEbicsHkd(requestContext)
-        "C53" -> handleEbicsC53(requestContext)
-        "C52" -> handleEbicsC52(requestContext)
-        "TSD" -> handleEbicsTSD()
-        "PTK" -> handleEbicsPTK()
-        else -> throw EbicsInvalidXmlError()
-    }
-    val compressedResponse = DeflaterInputStream(response.inputStream()).use {
-        it.readAllBytes()
-    }
-    val enc = CryptoUtil.encryptEbicsE002(compressedResponse, 
requestContext.clientEncPub)
-    val encodedResponse = Base64.getEncoder().encodeToString(enc.encryptedData)
-
-    val segmentSize = 4096
-    val totalSize = encodedResponse.length
-    val numSegments = ((totalSize + segmentSize - 1) / segmentSize)
-
-    EbicsDownloadTransactionEntity.new(transactionID) {
-        this.subscriber = requestContext.subscriber
-        this.host = requestContext.ebicsHost
-        this.orderType = orderType
-        this.segmentSize = segmentSize
-        this.transactionKeyEnc = ExposedBlob(enc.encryptedTransactionKey)
-        this.encodedResponse = encodedResponse
-        this.numSegments = numSegments
-        this.receiptReceived = false
-    }
-    /**
-     * In case of C52, the payload (that includes all the pending
-     * transactions) got at this point persisted into the database.
-     * The next block causes such transactions NOT to be returned
-     * along the next C52 request.
-     */
-    if (orderType == "C52") {
-        val account = getBankAccountFromSubscriber(requestContext.subscriber)
-        BankAccountFreshTransactionEntity.all().forEach {
-            if (it.transactionRef.account.label == account.label)
-                it.delete()
-        }
-    }
-    return EbicsResponse.createForDownloadInitializationPhase(
-        transactionID,
-        numSegments,
-        segmentSize,
-        enc, // has customer key
-        encodedResponse
-    )
-}
-
-private fun handleEbicsUploadTransactionInitialization(requestContext: 
RequestContext): EbicsResponse {
-    val orderType =
-        requestContext.requestObject.header.static.orderDetails?.orderType ?: 
throw EbicsInvalidRequestError()
-    val transactionID = EbicsOrderUtil.generateTransactionId()
-    logger.debug("Handling upload initialization for order $orderType, " +
-            "transactionID $transactionID, nonce: " +
-            (requestContext.requestObject.header.static.nonce?.toHexString() 
?: "not given")
-    )
-    val oidn = requestContext.subscriber.nextOrderID++
-    if (EbicsOrderUtil.checkOrderIDOverflow(oidn)) throw NotImplementedError()
-    val orderID = EbicsOrderUtil.computeOrderIDFromNumber(oidn)
-    val numSegments =
-        requestContext.requestObject.header.static.numSegments ?: throw 
EbicsInvalidRequestError()
-    val transactionKeyEnc =
-        
requestContext.requestObject.body.dataTransfer?.dataEncryptionInfo?.transactionKey
-            ?: throw EbicsInvalidRequestError()
-    val encPubKeyDigest =
-        
requestContext.requestObject.body.dataTransfer?.dataEncryptionInfo?.encryptionPubKeyDigest?.value
-            ?: throw EbicsInvalidRequestError()
-    val encSigData = 
requestContext.requestObject.body.dataTransfer?.signatureData?.value
-        ?: throw EbicsInvalidRequestError()
-    val decryptedSignatureData = CryptoUtil.decryptEbicsE002(
-        CryptoUtil.EncryptionResult(
-            transactionKeyEnc,
-            encPubKeyDigest,
-            encSigData
-        ), requestContext.hostEncPriv
-    )
-    val plainSigData = 
InflaterInputStream(decryptedSignatureData.inputStream()).use {
-        it.readAllBytes()
-    }
-    EbicsUploadTransactionEntity.new(transactionID) {
-        this.host = requestContext.ebicsHost
-        this.subscriber = requestContext.subscriber
-        this.lastSeenSegment = 0
-        this.orderType = orderType
-        this.orderID = orderID
-        this.numSegments = numSegments.toInt()
-        this.transactionKeyEnc = ExposedBlob(transactionKeyEnc)
-    }
-    val sigObj = 
XMLUtil.convertStringToJaxb<UserSignatureData>(plainSigData.toString(Charsets.UTF_8))
-    for (sig in sigObj.value.orderSignatureList ?: listOf()) {
-        logger.debug("inserting order signature for orderID $orderID, order 
type $orderType, transaction '$transactionID'")
-        EbicsOrderSignatureEntity.new {
-            this.orderID = orderID
-            this.orderType = orderType
-            this.partnerID = sig.partnerID
-            this.userID = sig.userID
-            this.signatureAlgorithm = sig.signatureVersion
-            this.signatureValue = ExposedBlob(sig.signatureValue)
-        }
-    }
-    return EbicsResponse.createForUploadInitializationPhase(transactionID, 
orderID)
-}
-
-private fun handleEbicsUploadTransactionTransmission(requestContext: 
RequestContext): EbicsResponse {
-    val uploadTransaction = requestContext.uploadTransaction ?: throw 
EbicsInvalidRequestError()
-    val requestObject = requestContext.requestObject
-    val requestSegmentNumber =
-        
requestContext.requestObject.header.mutable.segmentNumber?.value?.toInt() ?: 
throw EbicsInvalidRequestError()
-    val requestTransactionID = requestObject.header.static.transactionID ?: 
throw EbicsInvalidRequestError()
-    if (requestSegmentNumber == 1 && uploadTransaction.numSegments == 1) {
-        val encOrderData =
-            requestObject.body.dataTransfer?.orderData ?: throw 
EbicsInvalidRequestError()
-        val zippedData = CryptoUtil.decryptEbicsE002(
-            uploadTransaction.transactionKeyEnc.bytes,
-            Base64.getDecoder().decode(encOrderData),
-            requestContext.hostEncPriv
-        )
-        val unzippedData =
-            InflaterInputStream(zippedData.inputStream()).use { 
it.readAllBytes() }
-
-        val sigs = EbicsOrderSignatureEntity.find {
-            (EbicsOrderSignaturesTable.orderID eq uploadTransaction.orderID) 
and
-                    (EbicsOrderSignaturesTable.orderType eq 
uploadTransaction.orderType)
-        }
-        if (sigs.count() == 0L) {
-            throw EbicsInvalidRequestError()
-        }
-        for (sig in sigs) {
-            if (sig.signatureAlgorithm == "A006") {
-
-                val signedData = CryptoUtil.digestEbicsOrderA006(unzippedData)
-                val res1 = CryptoUtil.verifyEbicsA006(
-                    sig.signatureValue.bytes,
-                    signedData,
-                    requestContext.clientSigPub
-                )
-                if (!res1) {
-                    throw EbicsInvalidRequestError()
-                }
-
-            } else {
-                throw NotImplementedError()
-            }
-        }
-        if (getOrderTypeFromTransactionId(requestTransactionID) == "CCT") {
-            handleCct(unzippedData.toString(Charsets.UTF_8),
-                requestContext.subscriber
-            )
-        }
-        return EbicsResponse.createForUploadTransferPhase(
-            requestTransactionID,
-            requestSegmentNumber,
-            true,
-            uploadTransaction.orderID
-        )
-    } else {
-        throw NotImplementedError()
-    }
-}
-// req.header.static.hostID.
-private fun makeRequestContext(requestObject: EbicsRequest): RequestContext {
-    val staticHeader = requestObject.header.static
-    val requestedHostId = staticHeader.hostID
-    val ebicsHost =
-        EbicsHostEntity.find { EbicsHostsTable.hostID.upperCase() eq 
requestedHostId.uppercase(Locale.getDefault()) }
-            .firstOrNull()
-    val requestTransactionID = requestObject.header.static.transactionID
-    var downloadTransaction: EbicsDownloadTransactionEntity? = null
-    var uploadTransaction: EbicsUploadTransactionEntity? = null
-    val subscriber = if (requestTransactionID != null) {
-        downloadTransaction = 
EbicsDownloadTransactionEntity.findById(requestTransactionID.uppercase(Locale.getDefault()))
-        if (downloadTransaction != null) {
-            downloadTransaction.subscriber
-        } else {
-            uploadTransaction = 
EbicsUploadTransactionEntity.findById(requestTransactionID)
-            uploadTransaction?.subscriber
-        }
-    } else {
-        val partnerID = staticHeader.partnerID ?: throw 
EbicsInvalidRequestError()
-        val userID = staticHeader.userID ?: throw EbicsInvalidRequestError()
-        findEbicsSubscriber(partnerID, userID, staticHeader.systemID)
-    }
-
-    if (ebicsHost == null) throw EbicsInvalidRequestError()
-
-    /**
-     * NOTE: production logic must check against READY state (the
-     * one activated after the subscriber confirms their keys via post)
-     */
-    if (subscriber == null || subscriber.state != SubscriberState.INITIALIZED)
-        throw EbicsSubscriberStateError()
-
-    val hostAuthPriv = CryptoUtil.loadRsaPrivateKey(
-        ebicsHost.authenticationPrivateKey.bytes
-    )
-    val hostEncPriv = CryptoUtil.loadRsaPrivateKey(
-        ebicsHost.encryptionPrivateKey.bytes
-    )
-    val clientAuthPub =
-        
CryptoUtil.loadRsaPublicKey(subscriber.authenticationKey!!.rsaPublicKey.bytes)
-    val clientEncPub =
-        
CryptoUtil.loadRsaPublicKey(subscriber.encryptionKey!!.rsaPublicKey.bytes)
-    val clientSigPub =
-        
CryptoUtil.loadRsaPublicKey(subscriber.signatureKey!!.rsaPublicKey.bytes)
-
-    return RequestContext(
-        hostAuthPriv = hostAuthPriv,
-        hostEncPriv = hostEncPriv,
-        clientAuthPub = clientAuthPub,
-        clientEncPub = clientEncPub,
-        clientSigPub = clientSigPub,
-        ebicsHost = ebicsHost,
-        requestObject = requestObject,
-        subscriber = subscriber,
-        downloadTransaction = downloadTransaction,
-        uploadTransaction = uploadTransaction
-    )
-}
-
-suspend fun ApplicationCall.ebicsweb() {
-    val requestDocument = this.request.call.receive<Document>()
-    val requestedHostID = requestDocument.getElementsByTagName("HostID")
-    this.attributes.put(
-        EbicsHostIdAttribute,
-        requestedHostID.item(0).textContent
-    )
-    when (requestDocument.documentElement.localName) {
-        "ebicsUnsecuredRequest" -> {
-            val requestObject = 
requestDocument.toObject<EbicsUnsecuredRequest>()
-            logger.info("Serving a 
${requestObject.header.static.orderDetails.orderType} request")
-
-            val orderData = requestObject.body.dataTransfer.orderData.value
-            val header = requestObject.header
-
-            when (header.static.orderDetails.orderType) {
-                "INI" -> handleEbicsIni(header, orderData)
-                "HIA" -> handleEbicsHia(header, orderData)
-                else -> throw EbicsInvalidXmlError()
-            }
-        }
-        "ebicsHEVRequest" -> {
-            val hevResponse = HEVResponse().apply {
-                this.systemReturnCode = SystemReturnCodeType().apply {
-                    this.reportText = "[EBICS_OK]"
-                    this.returnCode = "000000"
-                }
-                this.versionNumber = 
listOf(HEVResponse.VersionNumber.create("H004", "02.50"))
-            }
-
-            val strResp = XMLUtil.convertJaxbToString(hevResponse)
-            if (!XMLUtil.validateFromString(strResp)) throw SandboxError(
-                HttpStatusCode.InternalServerError,
-                "Outgoing HEV response is invalid"
-            )
-            respondText(strResp, ContentType.Application.Xml, 
HttpStatusCode.OK)
-        }
-        // FIXME: should check subscriber state?
-        "ebicsNoPubKeyDigestsRequest" -> {
-            val requestObject = requestDocument.toObject<EbicsNpkdRequest>()
-            val hostInfo = ensureEbicsHost(requestObject.header.static.hostID)
-            when (requestObject.header.static.orderDetails.orderType) {
-                "HPB" -> handleEbicsHpb(hostInfo, requestDocument, 
requestObject.header)
-                else -> throw EbicsInvalidXmlError()
-            }
-        }
-        // FIXME: must check subscriber state.
-        "ebicsRequest" -> {
-            val requestObject = requestDocument.toObject<EbicsRequest>()
-            val responseXmlStr = 
transaction(Connection.TRANSACTION_SERIALIZABLE, repetitionAttempts = 10) {
-                // Step 1 of 3:  Get information about the host and subscriber
-                val requestContext = makeRequestContext(requestObject)
-                // Step 2 of 3:  Validate the signature
-                val verifyResult = 
XMLUtil.verifyEbicsDocument(requestDocument, requestContext.clientAuthPub)
-                if (!verifyResult) {
-                    throw EbicsAccountAuthorisationFailed("Subscriber's 
signature did not verify")
-                }
-                // Step 3 of 3:  Generate response
-                val ebicsResponse: EbicsResponse = when 
(requestObject.header.mutable.transactionPhase) {
-                    EbicsTypes.TransactionPhaseType.INITIALISATION -> {
-                        if (requestObject.header.static.numSegments == null) {
-                            
handleEbicsDownloadTransactionInitialization(requestContext)
-                        } else {
-                            
handleEbicsUploadTransactionInitialization(requestContext)
-                        }
-                    }
-                    EbicsTypes.TransactionPhaseType.TRANSFER -> {
-                        if (requestContext.uploadTransaction != null) {
-                            
handleEbicsUploadTransactionTransmission(requestContext)
-                        } else if (requestContext.downloadTransaction != null) 
{
-                            
handleEbicsDownloadTransactionTransfer(requestContext)
-                        } else {
-                            throw AssertionError()
-                        }
-                    }
-                    EbicsTypes.TransactionPhaseType.RECEIPT -> {
-                        val requestTransactionID =
-                            requestObject.header.static.transactionID ?: throw 
EbicsInvalidRequestError()
-                        if (requestContext.downloadTransaction == null)
-                            throw EbicsInvalidRequestError()
-                        logger.debug("Handling download receipt for EBICS 
transaction: " +
-                                requestTransactionID)
-                        /**
-                         * The receipt phase means that the client has already
-                         * received all the data related to the current 
download
-                         * transaction.  Hence this data can now be removed 
from
-                         * the database.
-                         */
-                        val ebicsData = transaction {
-                            
EbicsDownloadTransactionEntity.findById(requestTransactionID)
-                        }
-                        if (ebicsData == null)
-                            throw SandboxError(
-                                HttpStatusCode.InternalServerError,
-                                "EBICS transaction $requestTransactionID was 
not" +
-                                        "found in the database for deletion.",
-                                
LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE
-                            )
-                        ebicsData.delete()
-                        val receiptCode =
-                            requestObject.body.transferReceipt?.receiptCode ?: 
throw EbicsInvalidRequestError()
-                        
EbicsResponse.createForDownloadReceiptPhase(requestTransactionID, receiptCode 
== 0)
-                    }
-                }
-                signEbicsResponse(ebicsResponse, requestContext.hostAuthPriv)
-            }
-            if (!XMLUtil.validateFromString(responseXmlStr)) throw 
SandboxError(
-                HttpStatusCode.InternalServerError,
-                "Outgoing EBICS XML is invalid"
-            )
-            respondText(responseXmlStr, ContentType.Application.Xml, 
HttpStatusCode.OK)
-        }
-        else -> {
-            /* Log to console and return "unknown type" */
-            logger.info("Unknown message, just logging it!")
-            respond(
-                HttpStatusCode.NotImplemented,
-                SandboxError(
-                    HttpStatusCode.NotImplemented,
-                    "Not Implemented"
-                )
-            )
-        }
-    }
-}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt
deleted file mode 100644
index f0326a00..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt
+++ /dev/null
@@ -1,472 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2020 Taler Systems S.A.
- *
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
- *
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
- * Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING.  If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-package tech.libeufin.bank
-
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.databind.SerializationFeature
-import io.ktor.server.application.*
-import io.ktor.http.HttpStatusCode
-import io.ktor.server.request.*
-import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.util.*
-import java.security.interfaces.RSAPublicKey
-import java.util.*
-import java.util.zip.DeflaterInputStream
-import kotlin.reflect.KProperty
-
-data class DemobankConfig(
-    val allowRegistrations: Boolean,
-    val currency: String,
-    val cashoutCurrency: String? = null,
-    val bankDebtLimit: Int,
-    val usersDebtLimit: Int,
-    val withSignupBonus: Boolean,
-    val demobankName: String, // demobank name.
-    val captchaUrl: String? = null,
-    val smsTan: String? = null, // fixme: move the config subcommand
-    val emailTan: String? = null, // fixme: same as above.
-    val suggestedExchangeBaseUrl: String? = null,
-    val suggestedExchangePayto: String? = null,
-    val nexusBaseUrl: String? = null,
-    val usernameAtNexus: String? = null,
-    val passwordAtNexus: String? = null,
-    val enableConversionService: Boolean = false
-)
-
-fun <T>getConfigValueOrThrow(configKey: KProperty<T?>): T {
-    return configKey.getter.call() ?: throw 
nullConfigValueError(configKey.name)
-}
-
-/**
- * Helps to communicate Camt values without having
- * to parse the XML each time one is needed.
- */
-data class SandboxCamt(
-    val camtMessage: String,
-    val messageId: String,
-    /**
-     * That is the number of SECONDS since Epoch.  This
-     * value is exactly what goes into the Camt document.
-     */
-    val creationTime: Long
-)
-
-/**
- * DB helper inserting a new "account" into the database.
- * The account is made of a 'customer' and 'bank account'
- * object.  The helper checks first that the username is
- * acceptable (chars, no institutional names, available
- * names); then checks that IBAN is available and then adds
- * the two database objects under the given demobank.  This
- * function contains the common logic shared by the Access
- * and Circuit API.  Additional data that is peculiar to one
- * API should be added separately.
- *
- * It returns a AccountPair type.  That contains the customer
- * object and the bank account; the caller may this way add custom
- * values to them.  */
-data class AccountPair(
-    val customer: DemobankCustomerEntity,
-    val bankAccount: BankAccountEntity
-)
-fun insertNewAccount(username: String,
-                     password: String,
-                     name: String? = null, // tests and access API may not 
give one.
-                     iban: String? = null,
-                     demobank: String = "default",
-                     isPublic: Boolean = false): AccountPair {
-    requireValidResourceName(username)
-    // Forbid institutional usernames.
-    if (username == "bank" || username == "admin") {
-        logger.info("Username: $username not allowed.")
-        throw forbidden("Username: $username is not allowed.")
-    }
-    return transaction {
-        val demobankFromDb = getDemobank(demobank)
-        // Bank's fault, because when this function gets
-        // called, the demobank must exist.
-        if (demobankFromDb == null) {
-            logger.error("Demobank '$demobank' not found.  Won't add account 
$username")
-            throw internalServerError("Demobank $demobank not found.  Won't 
add account $username")
-        }
-        // Generate a IBAN if the caller didn't provide one.
-        val newIban = iban ?: getIban()
-        // Check IBAN collisions.
-        val checkIbanExist = BankAccountEntity.find(BankAccountsTable.iban eq 
newIban).firstOrNull()
-        if (checkIbanExist != null) {
-            logger.info("IBAN $newIban not available.  Won't register username 
$username")
-            throw conflict("IBAN $iban not available.")
-        }
-        // Check username availability.
-        val checkCustomerExist = DemobankCustomerEntity.find {
-            DemobankCustomersTable.username eq username
-        }.firstOrNull()
-        if (checkCustomerExist != null) {
-            throw SandboxError(
-                HttpStatusCode.Conflict,
-                "Username $username not available."
-            )
-        }
-        val newCustomer = DemobankCustomerEntity.new {
-            this.username = username
-            passwordHash = CryptoUtil.hashpw(password)
-            this.name = name // nullable
-        }
-        // Actual account creation.
-        val newBankAccount = BankAccountEntity.new {
-            this.iban = newIban
-            /**
-             * For now, keep same semantics of Pybank: a username
-             * is AS WELL a bank account label.  In other words, it
-             * identifies a customer AND a bank account.  The reason
-             * to have the two values (label and owner) is to allow
-             * multiple bank accounts being owned by one customer.
-             */
-            label = username
-            owner = username
-            this.demoBank = demobankFromDb
-            this.isPublic = isPublic
-        }
-        if (demobankFromDb.config.withSignupBonus)
-            newBankAccount.bonus("${demobankFromDb.config.currency}:100")
-        AccountPair(customer = newCustomer, bankAccount = newBankAccount)
-    }
-}
-
-/**
- * Return true if access to the bank account can be granted,
- * false otherwise.
- *
- * Given the policy of having bank account names matching
- * their owner's username, this function enforces such policy
- * with the exception that 'admin' can access every bank
- * account.  A null username indicates disabled authentication
- * checks, hence it grants the access.
- */
-fun allowOwnerOrAdmin(username: String?, bankAccountLabel: String): Boolean {
-    if (username == null) return true
-    if (username == "admin") return true
-    return username == bankAccountLabel
-}
-
-/**
- * Throws exception if the credentials are wrong.
- *
- * Return:
- * - null if the authentication is disabled (during tests, for example).
- *   This facilitates tests because allows requests to lack entirely an
- *   Authorization header.
- * - the username of the authenticated user
- * - throw exception when the authentication fails
- *
- * Note: at this point it is ONLY checked whether the user provided
- * a valid password for the username mentioned in the Authorization header.
- * The actual access to the resources must be later checked by each handler.
- */
-fun ApplicationRequest.basicAuth(onlyAdmin: Boolean = false): String? {
-    val withAuth = this.call.ensureAttribute(WITH_AUTH_ATTRIBUTE_KEY)
-    if (!withAuth) {
-        logger.info("Authentication is disabled - assuming tests currently 
running.")
-        return null
-    }
-    val credentials = getHTTPBasicAuthCredentials(this)
-    if (credentials.first == "admin") {
-        // env must contain the admin password, because --with-auth is true.
-        val adminPassword: String = 
this.call.ensureAttribute(ADMIN_PASSWORD_ATTRIBUTE_KEY)
-        if (credentials.second != adminPassword) throw unauthorized(
-            "Admin authentication failed"
-        )
-        return credentials.first
-    }
-    if (onlyAdmin) throw forbidden("Only admin allowed.")
-    val passwordHash = transaction {
-        val customer = getCustomer(credentials.first)
-        customer.passwordHash
-    }
-    if (!CryptoUtil.checkPwOrThrow(credentials.second, passwordHash))
-        throw unauthorized("Customer '${credentials.first}' gave wrong 
credentials")
-    return credentials.first
-}
-
-fun sandboxAssert(condition: Boolean, reason: String) {
-    if (!condition) throw SandboxError(HttpStatusCode.InternalServerError, 
reason)
-}
-
-fun getOrderTypeFromTransactionId(transactionID: String): String {
-    val uploadTransaction = transaction {
-        EbicsUploadTransactionEntity.findById(transactionID)
-    } ?: throw SandboxError(
-        /**
-         * NOTE: at this point, it might even be the server's fault.
-         * For example, if it failed to store a ID earlier.
-         */
-        HttpStatusCode.NotFound,
-        "Could not retrieve order type for transaction: $transactionID"
-    )
-    return uploadTransaction.orderType
-}
-
-fun getHistoryElementFromTransactionRow(dbRow: BankAccountTransactionEntity): 
XLibeufinBankTransaction {
-    return XLibeufinBankTransaction(
-        subject = dbRow.subject,
-        creditorIban = dbRow.creditorIban,
-        creditorBic = dbRow.creditorBic,
-        creditorName = dbRow.creditorName,
-        debtorIban = dbRow.debtorIban,
-        debtorBic = dbRow.debtorBic,
-        debtorName = dbRow.debtorName,
-        date = dbRow.date.toString(),
-        amount = dbRow.amount,
-        currency = dbRow.currency,
-        // UID assigned by the bank itself.
-        uid = dbRow.accountServicerReference,
-        direction = 
XLibeufinBankDirection.convertCamtDirectionToXLibeufin(dbRow.direction),
-        // UIDs as gotten from a pain.001 (from EBICS connections.)
-        pmtInfId = dbRow.pmtInfId,
-        endToEndId = dbRow.endToEndId
-    )
-}
-
-fun printConfig(demobank: DemobankConfigEntity) {
-    val ret = ObjectMapper()
-    ret.configure(SerializationFeature.INDENT_OUTPUT, true)
-    println(
-        ret.writeValueAsString(object {
-            val currency = demobank.config.currency
-            val bankDebtLimit = demobank.config.bankDebtLimit
-            val usersDebtLimit = demobank.config.usersDebtLimit
-            val allowRegistrations = demobank.config.allowRegistrations
-            val name = demobank.name // always 'default'
-            val withSignupBonus = demobank.config.withSignupBonus
-            val captchaUrl = demobank.config.captchaUrl
-            val suggestedExchangeBaseUrl = 
demobank.config.suggestedExchangeBaseUrl
-            val suggestedExchangePayto = demobank.config.suggestedExchangePayto
-        })
-    )
-}
-
-fun getHistoryElementFromTransactionRow(
-    dbRow: BankAccountFreshTransactionEntity
-): XLibeufinBankTransaction {
-    return getHistoryElementFromTransactionRow(dbRow.transactionRef)
-}
-
-/**
- * Need to be called within a transaction {} block.  It
- * is acceptable to pass a bank account's label as the
- * parameter, because usernames can only own one bank
- * account whose label equals the owner's username.
- *
- * Future versions may relax this policy to allow one
- * customer to own multiple bank accounts.
- */
-fun getCustomer(username: String): DemobankCustomerEntity {
-    return maybeGetCustomer(username) ?: throw notFound("Customer 
'${username}' not found")
-}
-fun maybeGetCustomer(username: String): DemobankCustomerEntity? {
-    return transaction {
-        DemobankCustomerEntity.find {
-            DemobankCustomersTable.username eq username
-        }.firstOrNull()
-    }
-}
-
-/**
- * Get person name from a customer's username, or throw
- * exception if not found.
- */
-fun getPersonNameFromCustomer(customerUsername: String): String {
-    return when (customerUsername) {
-        "admin" -> "Admin"
-        else -> transaction {
-            val ownerCustomer = DemobankCustomerEntity.find(
-                DemobankCustomersTable.username eq customerUsername
-            ).firstOrNull() ?: run {
-                logger.error("Customer '${customerUsername}' not found, 
couldn't get their name.")
-                throw SandboxError(
-                    HttpStatusCode.InternalServerError,
-                    "'$customerUsername' not a customer."
-                )
-
-            }
-            ownerCustomer.name ?: "Never given."
-        }
-    }
-}
-
-fun getDefaultDemobank(): DemobankConfigEntity {
-    return transaction {
-        DemobankConfigEntity.find {
-            DemobankConfigsTable.name eq "default"
-        }.firstOrNull()
-    } ?: throw SandboxError(
-        HttpStatusCode.InternalServerError,
-        "Default demobank is missing."
-    )
-}
-
-fun getWithdrawalOperation(opId: String): TalerWithdrawalEntity {
-    val uuid = parseUuid(opId)
-    return transaction {
-        TalerWithdrawalEntity.find {
-            TalerWithdrawalsTable.wopid eq uuid
-        }.firstOrNull() ?: throw SandboxError(
-            HttpStatusCode.NotFound, "Withdrawal operation $opId not found."
-        )
-    }
-}
-
-fun getBankAccountFromPayto(paytoUri: String): BankAccountEntity {
-    val paytoParse = parsePayto(paytoUri)
-    return getBankAccountFromIban(paytoParse.iban)
-}
-
-fun getBankAccountFromIban(iban: String): BankAccountEntity {
-    return transaction {
-        BankAccountEntity.find(BankAccountsTable.iban eq iban).firstOrNull()
-    } ?: throw SandboxError(
-        HttpStatusCode.NotFound,
-        "Did not find a bank account for $iban"
-    )
-}
-
-/**
- * The argument 'withBankFault' represents the case where
- * _the bank_ must ensure that a resource (in this case a bank
- * account) exists.  For example, every 'customer' should have
- * a 'bank account', and if a customer is found without a bank
- * account, then the bank broke such condition.
- */
-fun getBankAccountFromLabel(
-    label: String,
-    demobank: String = "default",
-    withBankFault: Boolean = false
-): BankAccountEntity {
-    val maybeDemobank = getDemobank(demobank)
-    if (maybeDemobank == null) {
-        logger.error("Demobank '$demobank' not found")
-        throw SandboxError(
-            HttpStatusCode.NotFound,
-            "Demobank '$demobank' not found"
-        )
-    }
-    return getBankAccountFromLabel(
-        label,
-        maybeDemobank,
-        withBankFault
-    )
-}
-
-// Get bank account DAO, given its name and demobank.
-fun getBankAccountFromLabel(
-    label: String,
-    demobank: DemobankConfigEntity,
-    withBankFault: Boolean = false // documented along the other same-named 
function.
-): BankAccountEntity {
-    val maybeBankAccount = transaction {
-        BankAccountEntity.find(
-            BankAccountsTable.label eq label and (
-                    BankAccountsTable.demoBank eq demobank.id
-                    )
-        ).firstOrNull()
-    }
-    if (maybeBankAccount == null && withBankFault)
-        throw internalServerError(
-            "Bank account $label was not found, but it should."
-        )
-    if (maybeBankAccount == null)
-        throw notFound(
-            "Bank account $label was not found."
-        )
-    return maybeBankAccount
-}
-
-fun getBankAccountFromSubscriber(subscriber: EbicsSubscriberEntity): 
BankAccountEntity {
-    return transaction {
-        subscriber.bankAccount ?: throw SandboxError(
-            HttpStatusCode.NotFound,
-            "Subscriber doesn't have any bank account"
-        )
-    }
-}
-
-fun BankAccountEntity.bonus(amount: String) {
-    wireTransfer(
-        "admin",
-        this.label,
-        this.demoBank.name,
-        "Sign-up bonus",
-        amount
-    )
-}
-
-fun ensureDemobank(call: ApplicationCall): DemobankConfigEntity {
-    return ensureDemobank(call.expectUriComponent("demobankid"))
-}
-
-fun ensureDemobank(name: String): DemobankConfigEntity {
-    return transaction {
-        DemobankConfigEntity.find {
-            DemobankConfigsTable.name eq name
-        }.firstOrNull() ?: throw notFound("Demobank '$name' not found.  Was it 
ever created?")
-    }
-}
-
-fun getDemobank(name: String?): DemobankConfigEntity? {
-    return transaction {
-        if (name == null) {
-            DemobankConfigEntity.all().firstOrNull()
-        } else {
-            DemobankConfigEntity.find {
-                DemobankConfigsTable.name eq name
-            }.firstOrNull()
-        }
-    }
-}
-
-fun getEbicsSubscriberFromDetails(userID: String, partnerID: String, hostID: 
String): EbicsSubscriberEntity {
-    return transaction {
-        EbicsSubscriberEntity.find {
-            (EbicsSubscribersTable.userId eq userID) and 
(EbicsSubscribersTable.partnerId eq partnerID) and
-                    (EbicsSubscribersTable.hostId eq hostID)
-        }.firstOrNull() ?: throw SandboxError(
-            HttpStatusCode.NotFound,
-            "Ebics subscriber (${userID}, ${partnerID}, ${hostID}) not found"
-        )
-    }
-}
-
-/**
- * Compress, encrypt, encode a EBICS payload.  The payload
- * is assumed to be a Zip archive with only one entry.
- * Return the customer key (second element) along the data.
- */
-fun prepareEbicsPayload(
-    payload: String, pub: RSAPublicKey
-): Pair<String, CryptoUtil.EncryptionResult> {
-    val zipSingleton = mutableListOf(payload.toByteArray()).zip()
-    val compressedResponse = 
DeflaterInputStream(zipSingleton.inputStream()).use {
-        it.readAllBytes()
-    }
-    val enc = CryptoUtil.encryptEbicsE002(compressedResponse, pub)
-    return Pair(Base64.getEncoder().encodeToString(enc.encryptedData), enc)
-}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/JSON.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/JSON.kt
deleted file mode 100644
index b7f245f3..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/JSON.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2019 Stanisci and Dold.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING.  If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-package tech.libeufin.bank
-
-import tech.libeufin.util.PaymentInfo
-
-data class WithdrawalRequest(
-    /**
-     * Note: the currency is redundant, because at each point during
-     * the execution the Demobank should have a handle of the currency.
-     */
-    val amount: String // $CURRENCY:X.Y
-)
-data class BalanceJson(
-    val amount: String,
-    val credit_debit_indicator: String
-)
-data class Demobank(
-    val currency: String,
-    val name: String,
-    val userDebtLimit: Int,
-    val bankDebtLimit: Int,
-    val allowRegistrations: Boolean
-)
-/**
- * Used to show the list of Ebics hosts that exist
- * in the system.
- */
-data class EbicsHostsResponse(
-    val ebicsHosts: List<String>
-)
-
-data class EbicsHostCreateRequest(
-    val hostID: String,
-    val ebicsVersion: String
-)
-
-/**
- * List type that show all the payments existing in the system.
- */
-data class AccountTransactions(
-    val payments: MutableList<PaymentInfo> = mutableListOf()
-)
-
-/**
- * Used to create AND show one Ebics subscriber.
- */
-data class EbicsSubscriberInfo(
-    val hostID: String,
-    val partnerID: String,
-    val userID: String,
-    val systemID: String? = null,
-    val demobankAccountLabel: String
-)
-
-data class AdminGetSubscribers(
-    var subscribers: MutableList<EbicsSubscriberInfo> = mutableListOf()
-)
-
-/**
- * The following definition is obsolete because it
- * doesn't allow to specify a demobank that will host
- * the Ebics subscriber.  */
-data class EbicsSubscriberObsoleteApi(
-    val hostID: String,
-    val partnerID: String,
-    val userID: String,
-    val systemID: String? = null
-)
-
-/**
- * Allows the admin to associate a new bank account
- * to a EBICS subscriber.
- */
-data class EbicsBankAccountRequest(
-    val subscriber: EbicsSubscriberObsoleteApi,
-    val iban: String,
-    val bic: String,
-    val name: String,
-    /**
-     * This value labels the bank account to be created
-     * AND its owner.  The 'owner' is a bank's customer
-     * whose username equals this label AND has the rights
-     * over such bank accounts.
-     */
-    val label: String
-)
-
-data class CustomerRegistration(
-    val username: String,
-    val password: String,
-    val isPublic: Boolean = false,
-    // When missing, it's autogenerated.
-    val iban: String?,
-    // When missing, stays null in the DB.
-    val name: String?
-)
-
-// Could be used as a general bank account info container.
-data class PublicAccountInfo(
-    val balance: String,
-    val iban: String,
-    // Name / Label of the bank account _and_ of the
-    // Sandbox username that owns it.
-    val accountLabel: String
-    // more ..?
-)
-
-data class CamtParams(
-    // name/label of the bank account to query.
-    val bankaccount: String,
-    val type: Int,
-    // need range parameter
-)
-
-data class TalerWithdrawalStatus(
-    val selection_done: Boolean,
-    val transfer_done: Boolean,
-    val amount: String,
-    val wire_types: List<String> = listOf("iban"),
-    val suggested_exchange: String? = null,
-    val sender_wire: String? = null,
-    val aborted: Boolean,
-    // Not needed with CLI wallets.
-    val confirm_transfer_url: String?
-)
-
-data class TalerWithdrawalSelection(
-    val reserve_pub: String,
-    val selected_exchange: String?
-)
-
-data class SandboxConfig(
-    val currency: String,
-    val version: String,
-    val name: String
-)
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index ab3c15e6..8b228fa8 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -1,575 +1,111 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2019 Stanisci and Dold.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING.  If not, see
- * <http://www.gnu.org/licenses/>
- */
-
 package tech.libeufin.bank
 
-import UtilError
-import com.fasterxml.jackson.core.util.DefaultIndenter
-import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.module.kotlin.KotlinFeature
-import com.fasterxml.jackson.module.kotlin.KotlinModule
-import com.github.ajalt.clikt.core.CliktCommand
-import com.github.ajalt.clikt.core.context
-import com.github.ajalt.clikt.core.subcommands
-import com.github.ajalt.clikt.output.CliktHelpFormatter
-import com.github.ajalt.clikt.parameters.arguments.argument
-import com.github.ajalt.clikt.parameters.options.*
-import com.github.ajalt.clikt.parameters.types.int
-import execThrowableOrTerminate
-import io.ktor.server.application.*
 import io.ktor.http.*
 import io.ktor.serialization.jackson.*
+import io.ktor.server.application.*
 import io.ktor.server.plugins.*
+import io.ktor.server.plugins.requestvalidation.*
+import io.ktor.server.plugins.callloging.*
 import io.ktor.server.plugins.contentnegotiation.*
+import io.ktor.server.plugins.cors.routing.*
 import io.ktor.server.plugins.statuspages.*
 import io.ktor.server.request.*
 import io.ktor.server.response.*
 import io.ktor.server.routing.*
-import io.ktor.server.util.*
-import io.ktor.server.plugins.callloging.*
-import io.ktor.server.plugins.cors.routing.*
-import io.ktor.util.date.*
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.statements.api.ExposedBlob
-import org.jetbrains.exposed.sql.transactions.transaction
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import org.slf4j.event.Level
-import org.w3c.dom.Document
-import startServer
 import tech.libeufin.util.*
-import java.math.BigDecimal
-import java.net.URL
-import java.security.interfaces.RSAPublicKey
-import javax.xml.bind.JAXBContext
-import kotlin.system.exitProcess
+import javax.xml.bind.ValidationException
 
+// GLOBALS
 val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank")
-const val PROTOCOL_VERSION_UNIFIED = "0:0:0" // Every protocol is still using 
the same version.
-const val SANDBOX_DB_ENV_VAR_NAME = "LIBEUFIN_SANDBOX_DB_CONNECTION"
-private val adminPassword: String? = 
System.getenv("LIBEUFIN_SANDBOX_ADMIN_PASSWORD")
-var WITH_AUTH = true // Needed by helpers too, hence not making it private.
+val db = Database(System.getProperty("BANK_DB_CONNECTION_STRING"))
 
-// Internal error type.
-data class SandboxError(
-    val statusCode: HttpStatusCode,
-    val reason: String,
-    val errorCode: LibeufinErrorCode? = null
-) : Exception(reason)
-
-// HTTP response error type.
-data class SandboxErrorJson(val error: SandboxErrorDetailJson)
-data class SandboxErrorDetailJson(val type: String, val description: String)
-
-class DefaultExchange : CliktCommand("Set default Taler exchange for a 
demobank.") {
-    init { context { helpFormatter = CliktHelpFormatter(showDefaultValues = 
true) } }
-    private val exchangeBaseUrl by argument("EXCHANGE-BASEURL", "base URL of 
the default exchange")
-    private val exchangePayto by argument("EXCHANGE-PAYTO", "default 
exchange's payto-address")
-    private val demobank by option("--demobank", help = "Which demobank 
defaults to EXCHANGE").default("default")
+// TYPES
+data class ChallengeContactData(
+    val email: String? = null,
+    val phone: String? = null
+)
+data class RegisterAccountRequest(
+    val username: String,
+    val password: String,
+    val name: String,
+    val is_public: Boolean = false,
+    val is_taler_exchange: Boolean = false,
+    val challenge_contact_data: ChallengeContactData,
+    val cashout_payto_uri: String?,
+    val internal_payto_uri: String?
+)
 
-    override fun run() {
-        val dbConnString = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)
-        execThrowableOrTerminate {
-            dbCreateTables(dbConnString)
-            transaction {
-                val maybeDemobank: DemobankConfigEntity? = 
DemobankConfigEntity.find {
-                    DemobankConfigsTable.name eq demobank
-                }.firstOrNull()
-                if (maybeDemobank == null) {
-                    System.err.println("Error, demobank $demobank not found.")
-                    exitProcess(1)
-                }
-                val config = maybeDemobank.config
-                /**
-                 * Iterating over the config object's field that hold the 
exchange
-                 * base URL and Payto.  The iteration is only used to retrieve 
the
-                 * correct names of the DB column 'configKey', because this is 
named
-                 * after such fields.
-                 */
-                listOf(
-                    Pair(config::suggestedExchangeBaseUrl, exchangeBaseUrl),
-                    Pair(config::suggestedExchangePayto, exchangePayto)
-                ).forEach {
-                    val maybeConfigPair = DemobankConfigPairEntity.find {
-                        DemobankConfigPairsTable.demobankName eq demobank and(
-                                DemobankConfigPairsTable.configKey eq 
it.first.name)
-                    }.firstOrNull()
-                    /**
-                     * The DB doesn't contain any column to hold the exchange 
URL
-                     * or Payto, fail.  That should never happen, because the 
DB row
-                     * are created _after_ the DemobankConfig object that 
_does_ contain
-                     * such fields.
-                     */
-                    if (maybeConfigPair == null) {
-                        System.err.println("Config key '${it.first.name}' for 
demobank '$demobank' not found in DB.")
-                        exitProcess(1)
-                    }
-                    maybeConfigPair.configValue = it.second
-                }
-            }
-        }
-    }
-}
-
-class Config : CliktCommand("Insert one configuration (a.k.a. demobank) into 
the database.") {
-    init { context { helpFormatter = CliktHelpFormatter(showDefaultValues = 
true) } }
-    private val nameArgument by argument(
-        "NAME", help = "Name of this configuration.  Currently, only 'default' 
is admitted."
-    )
-    private val showOption by option(
-        "--show",
-        help = "Only show values, other options will be ignored."
-    ).flag("--no-show", default = false)
-    // FIXME: This really should not be a global option!
-    private val captchaUrlOption by option(
-        "--captcha-url", help = "Needed for browser wallets."
-    ).default("https://bank.demo.taler.net/";)
-    private val currencyOption by option("--currency").default("EUR")
-    private val bankDebtLimitOption by 
option("--bank-debt-limit").int().default(1000000)
-    private val usersDebtLimitOption by 
option("--users-debt-limit").int().default(1000)
-    private val allowRegistrationsOption by option(
-        "--with-registrations",
-        help = "(defaults to allow registrations)" /* mentioning here as help 
message did not.  */
-    ).flag("--without-registrations", default = true)
-    private val withSignupBonusOption by option(
-        "--with-signup-bonus",
-        help = "Award new customers with 100 units of currency! (defaults to 
NO bonus)"
-    ).flag("--without-signup-bonus", default = false)
-
-    override fun run() {
-        val dbConnString = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)
-        if (nameArgument != "default") {
-            System.err.println("This version admits only the 'default' name")
-            exitProcess(1)
-        }
-        execThrowableOrTerminate {
-            dbCreateTables(dbConnString)
-            val maybeDemobank = transaction { getDemobank(nameArgument) }
-            if (showOption) {
-                if (maybeDemobank != null) {
-                    printConfig(maybeDemobank)
-                } else {
-                    println("Demobank: $nameArgument not found.")
-                    System.exit(1)
-                }
-                return@execThrowableOrTerminate
-            }
-            if (bankDebtLimitOption < 0 || usersDebtLimitOption < 0) {
-                System.err.println("Debt numbers can't be negative.")
-                exitProcess(1)
-            }
-            /*
-               Warning if the CAPTCHA URL does not include the {wopid} 
placeholder.
-               Not a reason to fail because the bank may be run WITHOUT 
providing Taler.
-             */
-            if (!hasWopidPlaceholder(captchaUrlOption))
-                logger.warn("CAPTCHA URL doesn't have the WOPID placeholder." +
-                        "  Taler withdrawals decrease usability")
-
-            // The user asks to _set_ values, regardless of overriding or 
creating.
-            val config = DemobankConfig(
-                currency = currencyOption,
-                bankDebtLimit = bankDebtLimitOption,
-                usersDebtLimit = usersDebtLimitOption,
-                allowRegistrations = allowRegistrationsOption,
-                demobankName = nameArgument,
-                withSignupBonus = withSignupBonusOption,
-                captchaUrl = captchaUrlOption
-            )
-            /**
-             * The demobank didn't exist.  Now:
-             *   1, Store the config values in the database.
-             *   2, Store the demobank name in the database.
-             *   3, Create the admin bank account under this demobank.
-             */
-            if (maybeDemobank == null) {
-                transaction {
-                    insertConfigPairs(config)
-                    val demoBank = DemobankConfigEntity.new { this.name = 
nameArgument }
-                    BankAccountEntity.new {
-                        iban = getIban()
-                        label = "admin"
-                        owner = "admin" // Not backed by an actual customer 
object.
-                        // For now, the model assumes always one demobank
-                        this.demoBank = demoBank
-                    }
-                }
-            }
-            // Demobank exists: update its config values in the database.
-            else transaction { insertConfigPairs(config, override = true) }
-        }
-    }
+// Generates a new Payto-URI with IBAN scheme.
+fun genIbanPaytoUri(): String = "payto://iban/SANDBOXX/${getIban()}"
+fun parseTalerAmount(amount: String): TalerAmount {
+    val amountWithCurrencyRe = "^([A-Z]+):([0-9]+(\\.[0-9][0-9]?)?)$"
+    val match = Regex(amountWithCurrencyRe).find(amount) ?:
+    throw badRequest("Invalid amount")
+    val value = match.destructured.component2()
+    val fraction = match.destructured.component3().substring(1)
+    return TalerAmount(value.toLong(), fraction.toInt())
 }
 
 /**
- * This command generates Camt53 statements - for all the bank accounts -
- * every time it gets run. The statements are only stored into the database.
- * The user should then query either via Ebics or via the JSON interface,
- * in order to retrieve their statements.
+ * Performs the HTTP basic authentication.  Returns the
+ * authenticated customer on success, or null otherwise.
  */
-class Camt053Tick : CliktCommand(
-    "Make a new Camt.053 time tick; all the fresh transactions" +
-            " will be inserted in a new Camt.053 report"
-) {
-    override fun run() {
-        val dbConnString = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)
-        execThrowableOrTerminate { dbCreateTables(dbConnString) }
-        val newStatements = mutableMapOf<String, 
MutableList<XLibeufinBankTransaction>>()
-        /**
-         * For each bank account, extract the latest statement and
-         * include all the later transactions in a new statement.
-         * Build empty statement, if the account does not have any
-         * transaction yet.
-         */
-        transaction {
-            BankAccountEntity.all().forEach { accountIter ->
-                // Give this account a entry in the final output.
-                newStatements.putIfAbsent(accountIter.label, mutableListOf())
-                val lastStatement = BankAccountStatementEntity.find {
-                    BankAccountStatementsTable.bankAccount eq 
accountIter.id.value
-                }.lastOrNull()
-                val lastStatementTime = lastStatement?.creationTime ?: 0L
-                BankAccountTransactionEntity.find {
-                    
BankAccountTransactionsTable.date.greater(lastStatementTime) and(
-                            BankAccountTransactionsTable.account eq 
accountIter.id.value
-                    )
-                }.forEach {
-                    newStatements[accountIter.label]?.add(
-                        getHistoryElementFromTransactionRow(it)
-                    ) ?: run {
-                        logger.error("Array operation failed while building 
statements for account: ${accountIter.label}")
-                        System.err.println("Fatal array error while building 
the statement, please report.")
-                        exitProcess(1)
-                    }
-                }
-                /**
-                 * Resorting the closing (CLBD) balance of the last statement; 
will
-                 * become the PRCD balance of the _new_ one.
-                 */
-                val camtData = buildCamtString(
-                    53,
-                    accountIter.iban,
-                    newStatements[accountIter.label]!!,
-                    currency = accountIter.demoBank.config.currency
-                )
-                BankAccountStatementEntity.new {
-                    statementId = camtData.messageId
-                    creationTime = getSystemTimeNow().toInstant().epochSecond
-                    xmlMessage = camtData.camtMessage
-                    bankAccount = accountIter
-                }
-            }
-            BankAccountFreshTransactionsTable.deleteAll()
-        }
-    }
-}
-
-class MakeTransaction : CliktCommand("Wire-transfer money between Sandbox bank 
accounts") {
-    init {
-        context { helpFormatter = CliktHelpFormatter(showDefaultValues = true) 
}
-    }
-    private val creditAccount by option(help = "Label of the bank account 
receiving the payment").required()
-    private val debitAccount by option(help = "Label of the bank account 
issuing the payment").required()
-    private val demobankArg by option("--demobank", help = "Which Demobank 
books this transaction").default("default")
-    private val amount by argument("AMOUNT", "Amount, in the CUR:X.Y format")
-    private val subjectArg by argument("SUBJECT", "Payment's subject")
-
-    override fun run() {
+fun doBasicAuth(encodedCredentials: String): Customer? {
+    val plainUserAndPass = String(base64ToBytes(encodedCredentials), 
Charsets.UTF_8) // :-separated
+    val userAndPassSplit = plainUserAndPass.split(
+        ":",
         /**
-         * Merely connecting here (and NOT creating any table) because this
-         * command should only be run after actual bank accounts exist in the
-         * system, meaning therefore that the database got already set up.
+         * this parameter allows colons to occur in passwords.
+         * Without this, passwords that have colons would be split
+         * and become meaningless.
          */
-        execThrowableOrTerminate {
-            val pgConnString = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)
-            connectWithSchema(getJdbcConnectionFromPg(pgConnString))
-        }
-        // Refuse to operate without a default demobank.
-        val demobank = getDemobank("default")
-        if (demobank == null) {
-            System.err.println("Sandbox cannot operate without a 'default' 
demobank.")
-            System.err.println("Please make one with the 'libeufin-sandbox 
config' command.")
-            exitProcess(1)
-        }
-        try {
-            wireTransfer(debitAccount, creditAccount, demobankArg, subjectArg, 
amount)
-        } catch (e: SandboxError) {
-            System.err.println(e.message)
-            exitProcess(1)
-        } catch (e: Exception) {
-            System.err.println(e.message)
-            exitProcess(1)
-        }
-    }
-}
-
-class ResetTables : CliktCommand("Drop all the tables from the database") {
-    init {
-        context {
-            helpFormatter = CliktHelpFormatter(showDefaultValues = true)
-        }
-    }
-    override fun run() {
-        val dbConnString = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)
-        execThrowableOrTerminate {
-            dbDropTables(dbConnString)
-            dbCreateTables(dbConnString)
-        }
-    }
-}
-
-class Serve : CliktCommand("Run sandbox HTTP server") {
-    init {
-        context {
-            helpFormatter = CliktHelpFormatter(showDefaultValues = true)
-        }
-    }
-    private val auth by option(
-        "--auth",
-        help = "Disable authentication."
-    ).flag("--no-auth", default = true)
-    private val localhostOnly by option(
-        "--localhost-only",
-        help = "Bind only to localhost.  On all interfaces otherwise"
-    ).flag("--no-localhost-only", default = true)
-    private val ipv4Only by option(
-        "--ipv4-only",
-        help = "Bind only to ipv4"
-    ).flag(default = false)
-    private val logLevel by option(
-        help = "Set the log level to: 'off', 'error', 'warn', 'info', 'debug', 
'trace', 'all'"
-    )
-    private val port by option().int().default(5000)
-    private val withUnixSocket by option(
-        help = "Bind the Sandbox to the Unix domain socket at PATH.  
Overrides" +
-                " --port, when both are given", metavar = "PATH"
+        limit = 2
     )
-    private val smsTan by option(help = "Command to send the TAN via SMS." +
-            "  The command gets the TAN via STDIN and the phone number" +
-            " as its first parameter"
-    )
-    private val emailTan by option(help = "Command to send the TAN via 
e-mail." +
-            "  The command gets the TAN via STDIN and the e-mail address as 
its" +
-            " first parameter.")
-    override fun run() {
-        WITH_AUTH = auth
-        setLogLevel(logLevel)
-        if (WITH_AUTH && adminPassword == null) {
-            System.err.println(
-                "Error: auth is enabled, but env " +
-                        "LIBEUFIN_SANDBOX_ADMIN_PASSWORD is not."
-                        + " (Option --no-auth exists for tests)"
-            )
-            exitProcess(1)
-        }
-        execThrowableOrTerminate {
-            dbCreateTables(getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME))
-        }
-        // Refuse to operate without a 'default' demobank.
-        val demobank = getDemobank("default")
-        if (demobank == null) {
-            System.err.println("Sandbox cannot operate without a 'default' 
demobank.")
-            System.err.println("Please make one with the 'libeufin-sandbox 
config' command.")
-            exitProcess(1)
-        }
-        if (withUnixSocket != null) {
-            startServer(
-                withUnixSocket!!,
-                app = sandboxApp
-            )
-            exitProcess(0)
-        }
-        SMS_TAN_CMD = smsTan
-        EMAIL_TAN_CMD = emailTan
-
-        logger.info("Starting Sandbox on port ${this.port}")
-        startServerWithIPv4Fallback(
-            options = StartServerOptions(
-                ipv4OnlyOpt = this.ipv4Only,
-                localhostOnlyOpt = this.localhostOnly,
-                portOpt = this.port
-            ),
-            app = sandboxApp
-        )
-    }
+    if (userAndPassSplit.size != 2) throw badRequest("Malformed Basic auth 
credentials found in the Authorization header.")
+    val login = userAndPassSplit[0]
+    val plainPassword = userAndPassSplit[1]
+    return db.customerPwAuth(login, CryptoUtil.hashpw(plainPassword))
+}
+
+/* Performs the bearer-token authentication.  Returns the
+ * authenticated customer on success, null otherwise. */
+fun doTokenAuth(
+    token: String,
+    requiredScope: TokenScope, // readonly or readwrite
+): Customer? {
+    val maybeToken: BearerToken = 
db.bearerTokenGet(token.toByteArray(Charsets.UTF_8)) ?: return null
+    val isExpired: Boolean = maybeToken.expirationTime - getNow().toMicro() < 0
+    if (isExpired || maybeToken.scope != requiredScope) return null // FIXME: 
mention the reason?
+    // Getting the related username.
+    return db.customerGetFromRowId(maybeToken.bankCustomer)
+        ?: throw internalServerError("Customer not found, despite token 
mentions it.")
 }
 
-private fun getJsonFromDemobankConfig(fromDb: DemobankConfigEntity): Demobank {
-    return Demobank(
-        currency = fromDb.config.currency,
-        userDebtLimit = fromDb.config.usersDebtLimit,
-        bankDebtLimit = fromDb.config.bankDebtLimit,
-        allowRegistrations = fromDb.config.allowRegistrations,
-        name = fromDb.name
-    )
-}
-fun findEbicsSubscriber(partnerID: String, userID: String, systemID: String?): 
EbicsSubscriberEntity? {
-    return if (systemID == null) {
-        EbicsSubscriberEntity.find {
-            (EbicsSubscribersTable.partnerId eq partnerID) and 
(EbicsSubscribersTable.userId eq userID)
-        }
-    } else {
-        EbicsSubscriberEntity.find {
-            (EbicsSubscribersTable.partnerId eq partnerID) and
-                    (EbicsSubscribersTable.userId eq userID) and
-                    (EbicsSubscribersTable.systemId eq systemID)
-        }
-    }.firstOrNull()
-}
-
-data class SubscriberKeys(
-    val authenticationPublicKey: RSAPublicKey,
-    val encryptionPublicKey: RSAPublicKey,
-    val signaturePublicKey: RSAPublicKey
-)
-
-data class EbicsHostPublicInfo(
-    val hostID: String,
-    val encryptionPublicKey: RSAPublicKey,
-    val authenticationPublicKey: RSAPublicKey
-)
-
-data class BankAccountInfo(
-    val label: String,
-    val name: String,
-    val iban: String,
-    val bic: String,
-)
-
-inline fun <reified T> Document.toObject(): T {
-    val jc = JAXBContext.newInstance(T::class.java)
-    val m = jc.createUnmarshaller()
-    return m.unmarshal(this, T::class.java).value
-}
-
-fun ensureNonNull(param: String?): String {
-    return param ?: throw SandboxError(
-        HttpStatusCode.BadRequest, "Bad ID given: $param"
-    )
-}
-
-class SandboxCommand : CliktCommand(invokeWithoutSubcommand = true, 
printHelpOnEmptyArgs = true) {
-    init { versionOption(getVersion()) }
-    override fun run() = Unit
-}
-
-fun main(args: Array<String>) {
-    SandboxCommand().subcommands(
-        Serve(),
-        ResetTables(),
-        Config(),
-        MakeTransaction(),
-        Camt053Tick(),
-        DefaultExchange()
-    ).main(args)
-}
-
-fun setJsonHandler(ctx: ObjectMapper) {
-    ctx.enable(SerializationFeature.INDENT_OUTPUT)
-    ctx.setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
-        indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
-        indentObjectsWith(DefaultIndenter("  ", "\n"))
-    })
-    ctx.registerModule(
-        KotlinModule.Builder()
-            .withReflectionCacheSize(512)
-            .configure(KotlinFeature.NullToEmptyCollection, false)
-            .configure(KotlinFeature.NullToEmptyMap, false)
-            .configure(KotlinFeature.NullIsSameAsDefault, enabled = true)
-            .configure(KotlinFeature.SingletonSupport, enabled = false)
-            .configure(KotlinFeature.StrictNullChecks, false)
-            .build()
-    )
-}
-
-private suspend fun getWithdrawal(call: ApplicationCall) {
-    val op = getWithdrawalOperation(call.expectUriComponent("withdrawal_id"))
-    if (!op.selectionDone && op.reservePub != null) throw internalServerError(
-        "Unselected withdrawal has a reserve public key",
-        LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE
-    )
-    call.respond(object {
-        val amount = op.amount
-        val aborted = op.aborted
-        val confirmation_done = op.confirmationDone
-        val selection_done = op.selectionDone
-        val selected_reserve_pub = op.reservePub
-        val selected_exchange_account = op.selectedExchangePayto
-    })
-}
-
-private suspend fun confirmWithdrawal(call: ApplicationCall) {
-    val withdrawalId = call.expectUriComponent("withdrawal_id")
-    logger.debug("Maybe confirming withdrawal: $withdrawalId")
-    transaction {
-        val wo = getWithdrawalOperation(withdrawalId)
-        if (wo.aborted) throw SandboxError(
-            HttpStatusCode.Conflict,
-            "Cannot confirm an aborted withdrawal."
-        )
-        if (!wo.selectionDone) throw SandboxError(
-            HttpStatusCode.UnprocessableEntity,
-            "Cannot confirm a unselected withdrawal: " +
-                    "specify exchange and reserve public key via Integration 
API first."
-        )
-        /**
-         * The wallet chose not to select any exchange, use the default.
-         */
-        val demobank = ensureDemobank(call)
-        if (wo.selectedExchangePayto == null) {
-            wo.selectedExchangePayto = demobank.config.suggestedExchangePayto
-        }
-        val exchangeBankAccount = getBankAccountFromPayto(
-            wo.selectedExchangePayto ?: throw internalServerError(
-                "Cannot withdraw without an exchange."
-            )
-        )
-        logger.debug("Withdrawal ${wo.wopid} confirmed? 
${wo.confirmationDone}")
-        if (!wo.confirmationDone) {
-            wireTransfer(
-                debitAccount = wo.walletBankAccount.label,
-                creditAccount = exchangeBankAccount.label,
-                amount = wo.amount,
-                subject = wo.reservePub ?: throw internalServerError(
-                    "Cannot transfer funds without reserve public key."
-                ),
-                // provide the currency.
-                demobank = ensureDemobank(call).name
-            )
-            wo.confirmationDone = true
-        }
-        wo.confirmationDone
+/**
+ * This function tries to authenticate the call according
+ * to the scheme that is mentioned in the Authorization header.
+ * The allowed schemes are either 'HTTP basic auth' or 'bearer token'.
+ *
+ * requiredScope can be either "readonly" or "readwrite".
+ *
+ * Returns the authenticated customer, or null if they failed.
+ */
+fun ApplicationCall.myAuth(requiredScope: TokenScope): Customer? {
+    // Extracting the Authorization header.
+    val header = getAuthorizationRawHeader(this.request)
+    val authDetails = getAuthorizationDetails(header)
+    return when (authDetails.scheme) {
+        "Basic" -> doBasicAuth(authDetails.content)
+        "Bearer" -> doTokenAuth(authDetails.content, requiredScope)
+        else -> throw badRequest("Authorization scheme '${authDetails.scheme}' 
is not supported.")
     }
-    call.respond(object {})
 }
 
-private suspend fun abortWithdrawal(call: ApplicationCall) {
-    val withdrawalId = call.expectUriComponent("withdrawal_id")
-    val operation = getWithdrawalOperation(withdrawalId)
-    if (operation.confirmationDone) throw conflict("Cannot abort paid 
withdrawal.")
-    transaction { operation.aborted = true }
-    call.respond(object {})
-}
 
-val sandboxApp: Application.() -> Unit = {
+val webApp: Application.() -> Unit = {
     install(CallLogging) {
         this.level = Level.DEBUG
         this.logger = tech.libeufin.bank.logger
@@ -587,1125 +123,72 @@ val sandboxApp: Application.() -> Unit = {
         allowCredentials = true
     }
     install(IgnoreTrailingSlash)
-    install(ContentNegotiation) {
-        register(ContentType.Text.Xml, XMLEbicsConverter())
-        /**
-         * Content type "text" must go to the XML parser
-         * because Nexus can't set explicitly the Content-Type
-         * (see https://github.com/ktorio/ktor/issues/1127) to
-         * "xml" and the request made gets somehow assigned the
-         * "text/plain" type:  */
-        register(ContentType.Text.Plain, XMLEbicsConverter())
-        jackson(contentType = ContentType.Application.Json) { 
setJsonHandler(this) }
-        /**
-         * Make jackson the default parser.  It runs also when
-         * the Content-Type request header is missing. */
-        jackson(contentType = ContentType.Any) { setJsonHandler(this) }
-    }
-    install(StatusPages) {
-        // Bank's fault: it should check the operands.  Respond 500
-        exception<ArithmeticException> { call, cause ->
-            logger.error("Exception while handling '${call.request.uri}', 
${cause.stackTraceToString()}")
-            call.respond(
-                HttpStatusCode.InternalServerError,
-                SandboxErrorJson(
-                    error = SandboxErrorDetailJson(
-                        type = "sandbox-error",
-                        description = cause.message ?: "Bank's error: 
arithmetic exception."
-                    )
-                )
-            )
-        }
-        // Not necessarily the bank's fault.
-        exception<SandboxError> { call, cause ->
-            logger.error("Exception while handling '${call.request.uri}', 
${cause.reason}")
-            call.respond(
-                cause.statusCode,
-                SandboxErrorJson(
-                    error = SandboxErrorDetailJson(
-                        type = "sandbox-error",
-                        description = cause.reason
-                    )
-                )
-            )
-        }
-        // Not necessarily the bank's fault.
-        exception<UtilError> { call, cause ->
-            logger.error("Exception while handling '${call.request.uri}', 
${cause.reason}")
-            call.respond(
-                cause.statusCode,
-                SandboxErrorJson(
-                    error = SandboxErrorDetailJson(
-                        type = "util-error",
-                        description = cause.reason
-                    )
-                )
-            )
-        }
-        /**
-         * Happens when a request fails to parse.  This branch triggers
-         * only when a JSON request fails.  XML problems are caught within
-         * the /ebicsweb handler and always ultimately rethrown as 
"EbicsRequestError",
-         * hence they do not reach this branch.
-         */
-        exception<BadRequestException> { call, wrapper ->
-            var rootCause = wrapper.cause
-            while (rootCause?.cause != null) rootCause = rootCause.cause
-            val errorMessage: String? = rootCause?.message ?: wrapper.message
-            if (errorMessage == null) {
-                logger.error("The bank didn't detect the cause of a bad 
request, fail.")
-                logger.error(wrapper.stackTraceToString())
-                throw SandboxError(
-                    HttpStatusCode.InternalServerError,
-                    "Did not find bad request details."
-                )
-            }
-            logger.error(errorMessage)
-            call.respond(
-                HttpStatusCode.BadRequest,
-                SandboxErrorJson(
-                    error = SandboxErrorDetailJson(
-                        type = "sandbox-error",
-                        description = errorMessage
-                    )
-                )
-            )
-        }
-        // Catch-all error, respond 500 because the bank didn't handle it.
-        exception<Throwable> { call, cause ->
-            logger.error("Unhandled exception while handling 
'${call.request.uri}'\n${cause.stackTraceToString()}")
-            call.respond(
-                HttpStatusCode.InternalServerError,
-                SandboxErrorJson(
-                    error = SandboxErrorDetailJson(
-                        type = "sandbox-error",
-                        description = cause.message ?: "Bank's error: 
unhandled exception."
-                    )
-                )
-            )
-        }
-        exception<EbicsRequestError> { call, cause ->
-            logger.error("Handling EbicsRequestError: ${cause.message}")
-            respondEbicsTransfer(call, cause.errorText, cause.errorCode)
-        }
-    }
-    intercept(ApplicationCallPipeline.Setup) {
-        val ac: ApplicationCall = call
-        ac.attributes.put(WITH_AUTH_ATTRIBUTE_KEY, WITH_AUTH)
-        if (WITH_AUTH) {
-            if(adminPassword == null) {
-                throw internalServerError(
-                    "Sandbox has no admin password defined." +
-                            " Please define LIBEUFIN_SANDBOX_ADMIN_PASSWORD in 
the environment, " +
-                            "or launch with --no-auth."
-
-                )
-            }
-            ac.attributes.put(ADMIN_PASSWORD_ATTRIBUTE_KEY, adminPassword)
-        }
-        return@intercept
-    }
-    intercept(ApplicationCallPipeline.Fallback) {
-        if (this.call.response.status() == null) {
-            call.respondText(
-                "Not found (no route matched).\n",
-                io.ktor.http.ContentType.Text.Plain,
-                io.ktor.http.HttpStatusCode.NotFound
-            )
-            return@intercept finish()
-        }
-    }
+    install(ContentNegotiation) { jackson {} }
     routing {
-        get("/") {
-            call.respondText(
-                "Hello, this is the Sandbox\n",
-                ContentType.Text.Plain
-            )
-        }
-        // Respond with the last statement of the requesting account.
-        // Query details in the body.
-        post("/admin/payments/camt") {
-            val username = call.request.basicAuth()
-            val body = call.receive<CamtParams>()
-            if (body.type != 53) throw SandboxError(
-                HttpStatusCode.NotFound,
-                "Only Camt.053 documents can be generated."
-            )
-            if (!allowOwnerOrAdmin(username, body.bankaccount))
-                throw unauthorized("User '${username}' has no rights over" +
-                        " bank account '${body.bankaccount}'")
-            val camtMessage = transaction {
-                val bankaccount = getBankAccountFromLabel(
-                    body.bankaccount,
-                    getDefaultDemobank()
-                )
-                BankAccountStatementEntity.find {
-                    BankAccountStatementsTable.bankAccount eq bankaccount.id
-                }.lastOrNull()?.xmlMessage ?: throw SandboxError(
-                    HttpStatusCode.NotFound,
-                    "Could not find any statements; please wait next tick"
-                )
-            }
-            call.respondText(
-                camtMessage, ContentType.Text.Xml, HttpStatusCode.OK
-            )
+        post("/accounts") {
+            // check if only admin.
+            val maybeOnlyAdmin = db.configGet("only_admin_registrations")
+            if (maybeOnlyAdmin?.lowercase() == "yes") {
+                val customer: Customer? = call.myAuth(TokenScope.readwrite)
+                if (customer == null || customer.login != "admin")
+                    // OK to leak the only-admin policy here?
+                    throw forbidden("Only admin allowed, and it failed to 
authenticate.")
+            }
+            // auth passed, proceed with activity.
+            val req = call.receive<RegisterAccountRequest>()
+            // Prohibit reserved usernames:
+            if (req.username == "admin" || req.username == "bank")
+                throw conflict("Username '${req.username}' is reserved.")
+            // Checking imdepotency.
+            val maybeCustomerExists = db.customerGetFromLogin(req.username)
+            if (maybeCustomerExists != null) {
+                val bankingInfo = 
db.bankAccountGetFromOwnerId(maybeCustomerExists.expectRowId())
+                    ?: throw internalServerError("Existing customer had no 
bank account!")
+                // Checking _all_ the details are the same.
+                val isIdentic =
+                    maybeCustomerExists.name == req.name &&
+                    maybeCustomerExists.email == 
req.challenge_contact_data.email &&
+                    maybeCustomerExists.phone == 
req.challenge_contact_data.phone &&
+                    maybeCustomerExists.cashoutPayto == req.cashout_payto_uri 
&&
+                    maybeCustomerExists.passwordHash == 
CryptoUtil.hashpw(req.password) &&
+                    bankingInfo.isPublic == req.is_public &&
+                    bankingInfo.isTalerExchange == req.is_taler_exchange &&
+                    bankingInfo.internalPaytoUri == req.internal_payto_uri
+                if (isIdentic) call.respond(HttpStatusCode.Created)
+                call.respond(HttpStatusCode.Conflict)
+            }
+            // From here: fresh user being added.
+            val newCustomer = Customer(
+                login = req.username,
+                name = req.name,
+                email = req.challenge_contact_data.email,
+                phone = req.challenge_contact_data.phone,
+                cashoutPayto = req.cashout_payto_uri,
+                // Following could be gone, if included in cashout_payto
+                cashoutCurrency = db.configGet("cashout_currency"),
+                passwordHash = CryptoUtil.hashpw(req.password)
+            )
+            val newCustomerRowId = db.customerCreate(newCustomer)
+                ?: throw internalServerError("New customer INSERT failed 
despite the previous checks")
+            /* Crashing here won't break data consistency between customers
+             * and bank accounts, because of the idempotency.  Client will
+             * just have to retry.  */
+            val maxDebt = db.configGet("max_debt_ordinary_customers").run {
+                if (this == null) throw internalServerError("Max debt not 
configured")
+                parseTalerAmount(this)
+            }
+            val newBankAccount = BankAccount(
+                hasDebt = false,
+                internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
+                owningCustomerId = newCustomerRowId,
+                isPublic = req.is_public,
+                isTalerExchange = req.is_taler_exchange,
+                maxDebt = maxDebt
+            )
+            if (!db.bankAccountCreate(newBankAccount))
+                throw internalServerError("Could not INSERT bank account 
despite all the checks.")
+            call.respond(HttpStatusCode.Created)
             return@post
         }
-
-        /**
-         * Create a new bank account, no EBICS relation.  Okay
-         * to let a user, since having a particular username allocates
-         * already a bank account with such label.
-         */
-        post("/admin/bank-accounts/{label}") {
-            val username = call.request.basicAuth()
-            val body = call.receive<BankAccountInfo>()
-            if (!allowOwnerOrAdmin(username, body.label))
-                throw unauthorized("User '$username' has no rights over" +
-                        " bank account '${body.label}'"
-                )
-            if (body.label == "admin" || body.label == "bank") throw forbidden(
-                "Requested bank account label '${body.label}' not allowed."
-            )
-            transaction {
-                val maybeBankAccount = BankAccountEntity.find {
-                    BankAccountsTable.label eq body.label
-                }.firstOrNull()
-                if (maybeBankAccount != null)
-                    throw conflict("Bank account '${body.label}' exist 
already")
-                // owner username == bank account label
-                val maybeCustomer = DemobankCustomerEntity.find {
-                    DemobankCustomersTable.username eq body.label
-                }.firstOrNull()
-                if (maybeCustomer == null)
-                    throw notFound("Customer '${body.label}' not found," +
-                            " cannot own any bank account.")
-                BankAccountEntity.new {
-                    iban = body.iban
-                    bic = body.bic
-                    label = body.label
-                    owner = body.label
-                    demoBank = getDefaultDemobank()
-                }
-            }
-            call.respond(object {})
-            return@post
-        }
-
-        // Information about one bank account.
-        get("/admin/bank-accounts/{label}") {
-            val username = call.request.basicAuth()
-            val label = call.expectUriComponent("label")
-            val ret = transaction {
-                val demobank = getDefaultDemobank()
-                val bankAccount = getBankAccountFromLabel(label, demobank)
-                if (!allowOwnerOrAdmin(username, label))
-                    throw unauthorized("'${username}' has no rights over 
'$label'")
-                val balance = getBalance(bankAccount)
-                object {
-                    val balance = 
"${bankAccount.demoBank.config.currency}:${balance}"
-                    val iban = bankAccount.iban
-                    val bic = bankAccount.bic
-                    val label = bankAccount.label
-                }
-            }
-            call.respond(ret)
-            return@get
-        }
-
-        // Book one incoming payment for the requesting account.
-        // The debtor is not required to have a customer account at this 
Sandbox.
-        post("/admin/bank-accounts/{label}/simulate-incoming-transaction") {
-            call.request.basicAuth(onlyAdmin = true)
-            val body = call.receive<IncomingPaymentInfo>()
-            val accountLabel = ensureNonNull(call.parameters["label"])
-            val reqDebtorBic = body.debtorBic
-            if (reqDebtorBic != null && !validateBic(reqDebtorBic)) {
-                throw SandboxError(
-                    HttpStatusCode.BadRequest,
-                    "invalid BIC"
-                )
-            }
-            val amount = parseAmount(body.amount)
-            transaction {
-                val demobank = getDefaultDemobank()
-                val account = getBankAccountFromLabel(
-                    accountLabel, demobank
-                )
-                val randId = getRandomString(16)
-                val customer = getCustomer(accountLabel)
-                BankAccountTransactionEntity.new {
-                    creditorIban = account.iban
-                    creditorBic = account.bic
-                    creditorName = customer.name ?: "Name not given."
-                    debtorIban = body.debtorIban
-                    debtorBic = reqDebtorBic
-                    debtorName = body.debtorName
-                    subject = body.subject
-                    this.amount = amount.amount
-                    date = getSystemTimeNow().toInstant().toEpochMilli()
-                    accountServicerReference = "sandbox-$randId"
-                    this.account = account
-                    direction = "CRDT"
-                    this.demobank = demobank
-                    this.currency = demobank.config.currency
-                }
-            }
-            call.respond(object {})
-        }
-        // Associates a new bank account with an existing Ebics subscriber.
-        post("/admin/ebics/bank-accounts") {
-            call.request.basicAuth(onlyAdmin = true)
-            val body = call.receive<EbicsBankAccountRequest>()
-            val subscriber = getEbicsSubscriberFromDetails(
-                body.subscriber.userID,
-                body.subscriber.partnerID,
-                body.subscriber.hostID
-            )
-            val res = insertNewAccount(
-                username = body.label,
-                /**
-                 * This value makes only happy the account creator helper.
-                 * Logic using this OBSOLETE HTTP handler would NOT expect
-                 * to use this password anyway.  The reason is that such 
obsolete
-                 * tests access their banking data always through the EBICS
-                 * subscriber, needing therefore no HTTP basic password to 
operate.
-                 */
-                password = "not-used",
-                iban = body.iban
-            )
-            transaction { subscriber.bankAccount = res.bankAccount }
-            call.respond({})
-            return@post
-        }
-
-        // Information about all the default demobank's bank accounts
-        get("/admin/bank-accounts") {
-            call.request.basicAuth(onlyAdmin = true)
-            val accounts = mutableListOf<BankAccountInfo>()
-            transaction {
-                val demobank = getDefaultDemobank()
-                // Finds all the accounts of this demobank.
-                BankAccountEntity.find { BankAccountsTable.demoBank eq 
demobank.id }.forEach {
-                    accounts.add(
-                        BankAccountInfo(
-                            label = it.label,
-                            bic = it.bic,
-                            iban = it.iban,
-                            name = "Bank account owner's name"
-                        )
-                    )
-                }
-            }
-            call.respond(accounts)
-        }
-
-        // Details of all the transactions of one bank account.
-        get("/admin/bank-accounts/{label}/transactions") {
-            val username = call.request.basicAuth()
-            val ret = AccountTransactions()
-            val accountLabel = ensureNonNull(call.parameters["label"])
-            if (!allowOwnerOrAdmin(username, accountLabel))
-                throw unauthorized("Requesting user '${username}'" +
-                        " has no rights over bank account '${accountLabel}'"
-            )
-            transaction {
-                val demobank = getDefaultDemobank()
-                val account = getBankAccountFromLabel(accountLabel, demobank)
-                BankAccountTransactionEntity.find {
-                    BankAccountTransactionsTable.account eq account.id
-                }.forEach {
-                    ret.payments.add(
-                        PaymentInfo(
-                            accountLabel = account.label,
-                            creditorIban = it.creditorIban,
-                            accountServicerReference = 
it.accountServicerReference,
-                            paymentInformationId = it.pmtInfId,
-                            debtorIban = it.debtorIban,
-                            subject = it.subject,
-                            date = GMTDate(it.date).toHttpDate(),
-                            amount = it.amount,
-                            creditorBic = it.creditorBic,
-                            creditorName = it.creditorName,
-                            debtorBic = it.debtorBic,
-                            debtorName = it.debtorName,
-                            currency = it.currency,
-                            creditDebitIndicator = when (it.direction) {
-                                "CRDT" -> "credit"
-                                "DBIT" -> "debit"
-                                else -> throw Error("invalid direction")
-                            }
-                        )
-                    )
-                }
-            }
-            call.respond(ret)
-        }
-        /**
-         * Generate one incoming and one outgoing transactions for
-         * one bank account.  Counterparts do not need to have an account
-         * at this Sandbox.
-         */
-        post("/admin/bank-accounts/{label}/generate-transactions") {
-            call.request.basicAuth(onlyAdmin = true)
-            transaction {
-                val accountLabel = ensureNonNull(call.parameters["label"])
-                val demobank = getDefaultDemobank()
-                val account = getBankAccountFromLabel(accountLabel, demobank)
-                val transactionReferenceCrdt = getRandomString(8)
-                val transactionReferenceDbit = getRandomString(8)
-
-                run {
-                    val amount = kotlin.random.Random.nextLong(5, 25)
-                    BankAccountTransactionEntity.new {
-                        creditorIban = account.iban
-                        creditorBic = account.bic
-                        creditorName = "Creditor Name"
-                        debtorIban = "DE64500105178797276788"
-                        debtorBic = "DEUTDEBB101"
-                        debtorName = "Max Mustermann"
-                        subject = "sample transaction 
$transactionReferenceCrdt"
-                        this.amount = amount.toString()
-                        date = getSystemTimeNow().toInstant().toEpochMilli()
-                        accountServicerReference = transactionReferenceCrdt
-                        this.account = account
-                        direction = "CRDT"
-                        this.demobank = demobank
-                        currency = demobank.config.currency
-                    }
-                }
-
-                run {
-                    val amount = kotlin.random.Random.nextLong(5, 25)
-
-                    BankAccountTransactionEntity.new {
-                        debtorIban = account.iban
-                        debtorBic = account.bic
-                        debtorName = "Debitor Name"
-                        creditorIban = "DE64500105178797276788"
-                        creditorBic = "DEUTDEBB101"
-                        creditorName = "Max Mustermann"
-                        subject = "sample transaction 
$transactionReferenceDbit"
-                        this.amount = amount.toString()
-                        date = getSystemTimeNow().toInstant().toEpochMilli()
-                        accountServicerReference = transactionReferenceDbit
-                        this.account = account
-                        direction = "DBIT"
-                        this.demobank = demobank
-                        currency = demobank.config.currency
-                    }
-                }
-            }
-            call.respond(object {})
-        }
-
-        /**
-         * Create a new EBICS subscriber without associating
-         * a bank account to it.  Currently every registered
-         * user is allowed to call this.
-         */
-        post("/admin/ebics/subscribers") {
-            call.request.basicAuth(onlyAdmin = true)
-            val body = call.receive<EbicsSubscriberObsoleteApi>()
-            transaction {
-                // Check the host ID exists.
-                EbicsHostEntity.find {
-                    EbicsHostsTable.hostID eq body.hostID
-                }.firstOrNull() ?: throw notFound("Host ID ${body.hostID} not 
found.")
-                // Check it exists first.
-                val maybeSubscriber = EbicsSubscriberEntity.find {
-                    EbicsSubscribersTable.userId eq body.userID and (
-                            EbicsSubscribersTable.partnerId eq body.partnerID
-                            ) and (EbicsSubscribersTable.systemId eq 
body.systemID) and
-                            (EbicsSubscribersTable.hostId eq body.hostID)
-                }.firstOrNull()
-                if (maybeSubscriber != null) throw conflict("EBICS subscriber 
exists already")
-                EbicsSubscriberEntity.new {
-                    partnerId = body.partnerID
-                    userId = body.userID
-                    systemId = null
-                    hostId = body.hostID
-                    state = SubscriberState.NEW
-                    nextOrderID = 1
-                }
-            }
-            call.respondText(
-                "Subscriber created.",
-                ContentType.Text.Plain, HttpStatusCode.OK
-            )
-            return@post
-        }
-
-        // Shows details of all the EBICS subscribers of this Sandbox.
-        get("/admin/ebics/subscribers") {
-            call.request.basicAuth(onlyAdmin = true)
-            val ret = AdminGetSubscribers()
-            transaction {
-                EbicsSubscriberEntity.all().forEach {
-                    ret.subscribers.add(
-                        EbicsSubscriberInfo(
-                            userID = it.userId,
-                            partnerID = it.partnerId,
-                            hostID = it.hostId,
-                            demobankAccountLabel = it.bankAccount?.label ?: 
"not associated yet"
-                        )
-                    )
-                }
-            }
-            call.respond(ret)
-            return@get
-        }
-
-        // Change keys used in the EBICS communications.
-        post("/admin/ebics/hosts/{hostID}/rotate-keys") {
-            call.request.basicAuth(onlyAdmin = true)
-            val hostID: String = call.parameters["hostID"] ?: throw 
SandboxError(
-                io.ktor.http.HttpStatusCode.BadRequest, "host ID missing in 
URL"
-            )
-            transaction {
-                val host = EbicsHostEntity.find {
-                    EbicsHostsTable.hostID eq hostID
-                }.firstOrNull() ?: throw SandboxError(
-                    HttpStatusCode.NotFound, "Host $hostID not found"
-                )
-                val pairA = CryptoUtil.generateRsaKeyPair(2048)
-                val pairB = CryptoUtil.generateRsaKeyPair(2048)
-                val pairC = CryptoUtil.generateRsaKeyPair(2048)
-                host.authenticationPrivateKey = 
ExposedBlob(pairA.private.encoded)
-                host.encryptionPrivateKey = ExposedBlob(pairB.private.encoded)
-                host.signaturePrivateKey = ExposedBlob(pairC.private.encoded)
-            }
-            call.respondText(
-                "Keys of '${hostID}' rotated.",
-                ContentType.Text.Plain,
-                HttpStatusCode.OK
-            )
-            return@post
-        }
-
-        // Create a new EBICS host
-        post("/admin/ebics/hosts") {
-            call.request.basicAuth(onlyAdmin = true)
-            val req = call.receive<EbicsHostCreateRequest>()
-            val pairA = CryptoUtil.generateRsaKeyPair(2048)
-            val pairB = CryptoUtil.generateRsaKeyPair(2048)
-            val pairC = CryptoUtil.generateRsaKeyPair(2048)
-            transaction {
-                val maybeHost = EbicsHostEntity.find {
-                    EbicsHostsTable.hostID eq req.hostID
-                }.firstOrNull()
-                if (maybeHost != null) {
-                    logger.info("EBICS host '${req.hostID}' exists already, 
this request conflicts.")
-                    throw conflict("EBICS host '${req.hostID}' exists already")
-                }
-                EbicsHostEntity.new {
-                    this.ebicsVersion = req.ebicsVersion
-                    this.hostId = req.hostID
-                    this.authenticationPrivateKey = 
ExposedBlob(pairA.private.encoded)
-                    this.encryptionPrivateKey = 
ExposedBlob(pairB.private.encoded)
-                    this.signaturePrivateKey = 
ExposedBlob(pairC.private.encoded)
-                }
-            }
-            call.respondText(
-                "Host '${req.hostID}' created.",
-                ContentType.Text.Plain,
-                HttpStatusCode.OK
-            )
-            return@post
-        }
-
-        // Show the names of all the Ebics hosts
-        get("/admin/ebics/hosts") {
-            call.request.basicAuth(onlyAdmin = true)
-            val ebicsHosts = transaction {
-                EbicsHostEntity.all().map { it.hostId }
-            }
-            call.respond(EbicsHostsResponse(ebicsHosts))
-        }
-        // Process one EBICS request
-        post("/ebicsweb") {
-            try { call.ebicsweb() }
-            /**
-             * The catch blocks try to extract a EBICS error message from the
-             * exception type being handled.  NOT logging under each catch 
block
-             * as ultimately the registered exception handler is expected to 
log. */
-            catch (e: UtilError) {
-                throw EbicsProcessingError("Serving EBICS threw unmanaged 
UtilError: ${e.reason}")
-            }
-            catch (e: SandboxError) {
-                val errorInfo: String = e.message ?: e.stackTraceToString()
-                logger.info(errorInfo)
-                // Should translate to EBICS error code.
-                when (e.errorCode) {
-                    LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE -> throw 
EbicsProcessingError("Invalid bank state.")
-                    LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE -> throw 
EbicsProcessingError("Inconsistent bank state.")
-                    else -> throw EbicsProcessingError("Unknown Libeufin error 
code: ${e.errorCode}.")
-                }
-            }
-            catch (e: EbicsNoDownloadDataAvailable) {
-                respondEbicsTransfer(call, e.errorText, e.errorCode)
-            }
-            catch (e: EbicsRequestError) {
-                /**
-                 * Preventing the last catch-all block from handling
-                 * a known error type.  Rethrowing here to let the top-level
-                 * handler take action.
-                 */
-                throw e
-            }
-            catch (e: Exception) {
-                logger.error(e.stackTraceToString())
-                throw EbicsProcessingError(e.message)
-            }
-            return@post
-        }
-
-        /**
-         * Create a new demobank instance with a particular currency,
-         * debt limit and possibly other configuration
-         * (could also be a CLI command for now)
-         */
-        post("/demobanks") {
-            throw NotImplementedError("Feature only available at the 
libeufin-sandbox CLI")
-        }
-
-        get("/demobanks") {
-            expectAdmin(call.request.basicAuth())
-            val ret = object { val demoBanks = mutableListOf<Demobank>() }
-            transaction {
-                DemobankConfigEntity.all().forEach {
-                    ret.demoBanks.add(getJsonFromDemobankConfig(it))
-                }
-            }
-            call.respond(ret)
-            return@get
-        }
-
-        get("/demobanks/{demobankid}") {
-            val demobank = ensureDemobank(call)
-            expectAdmin(call.request.basicAuth())
-            call.respond(getJsonFromDemobankConfig(demobank))
-            return@get
-        }
-
-        route("/demobanks/{demobankid}") {
-            // NOTE: TWG assumes that username == bank account label.
-            route("/taler-wire-gateway") {
-                post("/{exchangeUsername}/admin/add-incoming") {
-                    val username = call.expectUriComponent("exchangeUsername")
-                    val usernameAuth = call.request.basicAuth()
-                    if (username != usernameAuth)
-                        throw forbidden("Bank account name and username 
differ: $username vs $usernameAuth")
-                    logger.debug("TWG add-incoming passed authentication")
-                    val body = try { call.receive<TWGAdminAddIncoming>() }
-                    catch (e: Exception) {
-                        logger.error("/admin/add-incoming failed at parsing 
the request body")
-                        throw SandboxError(
-                            HttpStatusCode.BadRequest,
-                            "Invalid request"
-                        )
-                    }
-                    val singletonTx = transaction {
-                        val demobank = ensureDemobank(call)
-                        val bankAccountCredit = 
getBankAccountFromLabel(username, demobank)
-                        if (bankAccountCredit.owner != username) throw 
forbidden(
-                            "User '$username' cannot access bank account with 
label: $username."
-                        )
-                        val bankAccountDebit = 
getBankAccountFromPayto(body.debit_account)
-                        logger.debug("TWG add-incoming about to wire transfer")
-                        val ref = wireTransfer(
-                            bankAccountDebit.label,
-                            bankAccountCredit.label,
-                            demobank.name,
-                            body.reserve_pub,
-                            body.amount
-                        )
-                        /**
-                         * The remaining part aims at returning an 
x-libeufin-bank-formatted
-                         * message to Nexus, to let it ingest the (incoming 
side of the) payment
-                         * information.  The format choice makes it more 
practical for Nexus,
-                         * because it handles this format already for the 
x-libeufin-bank connection
-                         * type.
-                         */
-                        val incomingTx = BankAccountTransactionEntity.find {
-                            
BankAccountTransactionsTable.accountServicerReference eq ref and (
-                                    BankAccountTransactionsTable.direction eq 
"CRDT"
-                            ) // closes the 'and'.
-                        }.firstOrNull()
-                        if (incomingTx == null)
-                            throw internalServerError("Just created 
transaction not found in DB.  AcctSvcrRef: $ref")
-                        val incomingHistoryElement = 
getHistoryElementFromTransactionRow(incomingTx)
-                        logger.debug("TWG add-incoming has wire transferred, 
AcctSvcrRef: $ref")
-                        incomingHistoryElement
-                    }
-                    val resp = object {
-                        val transactions = listOf(singletonTx)
-                    }
-                    call.respond(resp)
-                    return@post
-                }
-            }
-            // Talk to wallets.
-            route("/integration-api") {
-                get("/config") {
-                    val demobank = ensureDemobank(call)
-                    call.respond(SandboxConfig(
-                        name = "taler-bank-integration",
-                        version = PROTOCOL_VERSION_UNIFIED,
-                        currency = demobank.config.currency
-                    ))
-                    return@get
-                }
-                post("/withdrawal-operation/{wopid}") {
-                    val arg = ensureNonNull(call.parameters["wopid"])
-                    val withdrawalUuid = parseUuid(arg)
-                    val body = call.receive<TalerWithdrawalSelection>()
-                    val transferDone = transaction {
-                        val wo = TalerWithdrawalEntity.find {
-                            TalerWithdrawalsTable.wopid eq withdrawalUuid
-                        }.firstOrNull() ?: throw SandboxError(
-                            HttpStatusCode.NotFound, "Withdrawal operation 
$withdrawalUuid not found."
-                        )
-                        if (wo.confirmationDone) {
-                            return@transaction true
-                        }
-                        if (wo.selectionDone) {
-                            if (body.reserve_pub != wo.reservePub) throw 
SandboxError(
-                                HttpStatusCode.Conflict,
-                                "Selecting a different reserve from the one 
already selected"
-                            )
-                            if (body.selected_exchange != 
wo.selectedExchangePayto) throw SandboxError(
-                                HttpStatusCode.Conflict,
-                                "Selecting a different exchange from the one 
already selected"
-                            )
-                            return@transaction false
-                        }
-                        // Flow here means never selected, hence must as well 
never be paid.
-                        if (wo.confirmationDone) throw internalServerError(
-                            "Withdrawal ${wo.wopid} knew NO exchange and 
reserve pub, " +
-                                    "but is marked as paid!"
-                        )
-                        wo.reservePub = body.reserve_pub
-                        wo.selectedExchangePayto = body.selected_exchange
-                        wo.selectionDone = true
-                        false
-                    }
-                    call.respond(object {
-                        val transfer_done: Boolean = transferDone
-                    })
-                    return@post
-                }
-                get("/withdrawal-operation/{wopid}") {
-                    val arg = ensureNonNull(call.parameters["wopid"])
-                    val maybeWithdrawalUuid = parseUuid(arg)
-                    val maybeWithdrawalOp = transaction {
-                        TalerWithdrawalEntity.find {
-                            TalerWithdrawalsTable.wopid eq maybeWithdrawalUuid
-                        }.firstOrNull() ?: throw SandboxError(
-                            HttpStatusCode.NotFound,
-                            "Withdrawal operation: $arg not found"
-                        )
-                    }
-                    val demobank = ensureDemobank(call)
-                    val captchaPage: String? = 
demobank.config.captchaUrl?.replace("{wopid}",arg)
-                    if (captchaPage == null)
-                        throw internalServerError("demobank ${demobank.name} 
lacks the CAPTCHA URL from the configuration.")
-                    val ret = TalerWithdrawalStatus(
-                        selection_done = maybeWithdrawalOp.selectionDone,
-                        transfer_done = maybeWithdrawalOp.confirmationDone,
-                        amount = maybeWithdrawalOp.amount,
-                        suggested_exchange = 
demobank.config.suggestedExchangeBaseUrl,
-                        aborted = maybeWithdrawalOp.aborted,
-                        confirm_transfer_url = captchaPage
-                    )
-                    call.respond(ret)
-                    return@get
-                }
-            }
-            route("/circuit-api") {
-                circuitApi(this)
-            }
-            // Talk to Web UI.
-            route("/access-api") {
-                post("/accounts/{account_name}/transactions") {
-                    val username = call.request.basicAuth()
-                    val demobank = ensureDemobank(call)
-                    val bankAccount = getBankAccountFromLabel(
-                        call.expectUriComponent("account_name"),
-                        demobank
-                    )
-                    // note: admin has no rights to create transactions on 
non-admin accounts.
-                    val authGranted: Boolean = !WITH_AUTH
-                    if (!authGranted && username != bankAccount.label)
-                        throw unauthorized("Username '$username' has no rights 
over bank account ${bankAccount.label}")
-                    val req = call.receive<XLibeufinBankPaytoReq>()
-                    val payto = parsePayto(req.paytoUri)
-                    val amount: String? = payto.amount ?: req.amount
-                    if (amount == null) throw badRequest("Amount is missing")
-                    /**
-                     * The transaction block below lets the 'demoBank' field
-                     * of 'bankAccount' be correctly accessed.  */
-                    transaction {
-                        wireTransfer(
-                            debitAccount = bankAccount.label,
-                            creditAccount = 
getBankAccountFromIban(payto.iban).label,
-                            demobank = bankAccount.demoBank.name,
-                            subject = payto.message ?: throw badRequest(
-                                "'message' query parameter missing in Payto 
address"
-                            ),
-                            amount = amount,
-                            pmtInfId = req.pmtInfId
-                        )
-                    }
-                    call.respond(object {})
-                    return@post
-                }
-                // Information about one withdrawal.
-                get("/accounts/{account_name}/withdrawals/{withdrawal_id}") {
-                    getWithdrawal(call)
-                    return@get
-                }
-                // account-less style:
-                get("/withdrawals/{withdrawal_id}") {
-                    getWithdrawal(call)
-                    return@get
-                }
-                // Create a new withdrawal operation.
-                post("/accounts/{account_name}/withdrawals") {
-                    var username = call.request.basicAuth()
-                    val demobank = ensureDemobank(call)
-                    /**
-                     * Check here if the user has the right over the claimed 
bank account.  After
-                     * this check, the withdrawal operation will be allowed 
only by providing its
-                     * UID. */
-                    val maybeOwnedAccount = getBankAccountFromLabel(
-                        call.expectUriComponent("account_name"),
-                        demobank
-                    )
-                    val authGranted = !WITH_AUTH // note: admin not allowed on 
non-admin accounts
-                    if (!authGranted && maybeOwnedAccount.owner != username)
-                        throw unauthorized("Customer '$username' has no rights 
over bank account '${maybeOwnedAccount.label}'")
-                    val req = call.receive<WithdrawalRequest>()
-                    // Check for currency consistency
-                    val amount = parseAmount(req.amount)
-                    if (amount.currency != demobank.config.currency)
-                        throw badRequest("Currency ${amount.currency} differs 
from Demobank's: ${demobank.config.currency}")
-                    // Check funds are sufficient.
-                    if (
-                        maybeDebit(
-                            maybeOwnedAccount.label,
-                            BigDecimal(amount.amount),
-                            transaction { maybeOwnedAccount.demoBank.name }
-                        )) {
-                        logger.error("Account ${maybeOwnedAccount.label} would 
surpass debit threshold.  Not withdrawing")
-                        throw SandboxError(HttpStatusCode.Conflict, 
"Insufficient funds")
-                    }
-                    val wo: TalerWithdrawalEntity = transaction {
-                        TalerWithdrawalEntity.new {
-                        this.amount = req.amount
-                        walletBankAccount = maybeOwnedAccount
-                        }
-                    }
-                    val baseUrl = URL(call.request.getBaseUrl())
-                    val withdrawUri = url {
-                        protocol = URLProtocol(
-                            name = "taler".plus(if 
(baseUrl.protocol.lowercase() == "http") "+http" else ""),
-                            defaultPort = -1
-                        )
-                        host = "withdraw"
-                        val pathSegments = mutableListOf(
-                            /**
-                             * encodes the hostname(+port) of the actual
-                             * bank that will serve the withdrawal request.
-                             */
-                            baseUrl.host.plus(
-                                if (baseUrl.port != -1)
-                                    ":${baseUrl.port}"
-                                else ""
-                            )
-                        )
-                        /**
-                         * Slashes can only be intermediate and single,
-                         * any other combination results in badly formed URIs.
-                         * The following loop ensure this for the current URI 
path.
-                         * This might even come from X-Forwarded-Prefix.
-                         */
-                        baseUrl.path.split("/").forEach {
-                            if (it.isNotEmpty()) pathSegments.add(it)
-                        }
-                        
pathSegments.add("demobanks/${demobank.name}/integration-api/${wo.wopid}")
-                        this.appendPathSegments(pathSegments)
-                    }
-                    call.respond(object {
-                        val withdrawal_id = wo.wopid.toString()
-                        val taler_withdraw_uri = withdrawUri
-                    })
-                    return@post
-                }
-                // Confirm a withdrawal: no basic auth, because the ID should 
be unguessable.
-                
post("/accounts/{account_name}/withdrawals/{withdrawal_id}/confirm") {
-                    confirmWithdrawal(call)
-                    return@post
-                }
-                // account-less style:
-                post("/withdrawals/{withdrawal_id}/confirm") {
-                    confirmWithdrawal(call)
-                    return@post
-                }
-                // Aborting withdrawals:
-                
post("/accounts/{account_name}/withdrawals/{withdrawal_id}/abort") {
-                    abortWithdrawal(call)
-                    return@post
-                }
-                // account-less style:
-                post("/withdrawals/{withdrawal_id}/abort") {
-                    abortWithdrawal(call)
-                    return@post
-                }
-                // Bank account basic information.
-                get("/accounts/{account_name}") {
-                    val username = call.request.basicAuth()
-                    val accountAccessed = 
call.expectUriComponent("account_name")
-                    val demobank = ensureDemobank(call)
-                    val bankAccount = getBankAccountFromLabel(accountAccessed, 
demobank)
-                    val authGranted = !WITH_AUTH || bankAccount.isPublic || 
username == "admin"
-                    if (!authGranted && bankAccount.owner != username)
-                        throw forbidden("Customer '$username' cannot access 
bank account '$accountAccessed'")
-                    val balance = getBalance(bankAccount)
-                    logger.debug("Balance of '$username': 
${balance.toPlainString()}")
-                    call.respond(object {
-                        val balance = object {
-                            val amount = 
"${demobank.config.currency}:${balance.abs().toPlainString()}"
-                            val credit_debit_indicator = if (balance < 
BigDecimal.ZERO) "debit" else "credit"
-                        }
-                        val paytoUri = buildIbanPaytoUri(
-                            iban = bankAccount.iban,
-                            bic = bankAccount.bic,
-                            // username 'null' should only happen when auth is 
disabled.
-                            receiverName = 
getPersonNameFromCustomer(bankAccount.owner)
-                        )
-                        val iban = bankAccount.iban
-                        // The Elvis operator helps the --no-auth case,
-                        // where username would be empty
-                        val debitThreshold = getMaxDebitForUser(
-                            username = username ?: "admin",
-                            demobankName = demobank.name
-                        ).toString()
-                    })
-                    return@get
-                }
-                get("/accounts/{account_name}/transactions/{tId}") {
-                    val username = call.request.basicAuth()
-                    val demobank = ensureDemobank(call)
-                    val bankAccount = getBankAccountFromLabel(
-                        call.expectUriComponent("account_name"),
-                        demobank
-                    )
-                    val authGranted: Boolean = bankAccount.isPublic || 
!WITH_AUTH || username == "admin"
-                    if (!authGranted && username != bankAccount.owner)
-                        throw forbidden("Cannot access bank account 
${bankAccount.label}")
-                    val tId = call.parameters["tId"] ?: throw badRequest("URI 
didn't contain the transaction ID")
-                    val tx: BankAccountTransactionEntity? = transaction {
-                        BankAccountTransactionEntity.find {
-                            
BankAccountTransactionsTable.accountServicerReference eq tId
-                        }.firstOrNull()
-                    }
-                    if (tx == null) throw notFound("Transaction $tId wasn't 
found")
-                    call.respond(getHistoryElementFromTransactionRow(tx))
-                    return@get
-                }
-                get("/accounts/{account_name}/transactions") {
-                    val username = call.request.basicAuth()
-                    val demobank = ensureDemobank(call)
-                    val bankAccount = getBankAccountFromLabel(
-                        call.expectUriComponent("account_name"),
-                        demobank
-                    )
-                    val authGranted: Boolean = bankAccount.isPublic || 
!WITH_AUTH || username == "admin"
-                    if (!authGranted && bankAccount.owner != username)
-                        throw forbidden("Cannot access bank account 
${bankAccount.label}")
-                    // Paging values.
-                    val page: Int = 
expectInt(call.request.queryParameters["page"] ?: "1")
-                    if (page < 1) throw badRequest("'page' param is less than 
1")
-                    val size: Int = 
expectInt(call.request.queryParameters["size"] ?: "5")
-                    if (size < 1) throw badRequest("'size' param is less than 
1")
-                    // Time range filter values
-                    val fromMs: Long = 
expectLong(call.request.queryParameters["from_ms"] ?: "0")
-                    if (fromMs < 0) throw badRequest("'from_ms' param is less 
than 0")
-                    val untilMs: Long = 
expectLong(call.request.queryParameters["until_ms"] ?: 
Long.MAX_VALUE.toString())
-                    if (untilMs < 0) throw badRequest("'until_ms' param is 
less than 0")
-                    val longPollMs: Long? = call.maybeLong("long_poll_ms")
-                    // LISTEN, if Postgres.
-                    val listenHandle = if (isPostgres() && longPollMs != null) 
{
-                        val channelName = buildChannelName(
-                            NotificationsChannelDomains.LIBEUFIN_REGIO_TX,
-                            call.expectUriComponent("account_name")
-                        )
-                        val listenHandle = PostgresListenHandle(channelName)
-                        // Can't LISTEN on the same DB TX that checks for 
data, as Exposed
-                        // closes that connection and the notification getter 
would fail.
-                        // Can't invoke the notification getter in the same DB 
TX either,
-                        // as it would block the DB.
-                        listenHandle.postgresListen()
-                        listenHandle
-                    } else null
-                    val historyParams = HistoryParams(
-                        pageNumber = page,
-                        pageSize = size,
-                        bankAccount = bankAccount,
-                        fromMs = fromMs,
-                        untilMs = untilMs
-                    )
-                    var ret: List<XLibeufinBankTransaction> = transaction {
-                        extractTxHistory(historyParams)
-                    }
-                    logger.debug("Is payment data empty? ${ret.isEmpty()}")
-                    // Data was found already, UNLISTEN and respond.
-                    if (listenHandle != null && ret.isNotEmpty()) {
-                        logger.debug("No need to wait DB events, payment data 
found.")
-                        listenHandle.postgresUnlisten()
-                        call.respond(object {val transactions = ret})
-                        return@get
-                    }
-                    // No data was found, sleep until the timeout or getting 
woken up.
-                    // Third condition only silences the compiler.
-                    if (listenHandle != null && longPollMs != null) {
-                        logger.debug("Waiting DB event for new payment data.")
-                        val notificationArrived = 
listenHandle.waitOnIODispatchers(longPollMs)
-                        // Only if the awaited event fired, query again the DB.
-                        if (notificationArrived)
-                        {
-                            ret = transaction {
-                                // Refreshing to update the index to the very 
last transaction.
-                                historyParams.bankAccount.refresh()
-                                extractTxHistory(historyParams)
-                            }
-                        }
-                    }
-                    call.respond(object {val transactions = ret})
-                    return@get
-                }
-                get("/public-accounts") {
-                    val demobank = ensureDemobank(call)
-                    val ret = object {
-                        val publicAccounts = mutableListOf<PublicAccountInfo>()
-                    }
-                    transaction {
-                        BankAccountEntity.find {
-                            BankAccountsTable.isPublic eq true and(
-                                    BankAccountsTable.demoBank eq demobank.id
-                            )
-                        }.forEach {
-                            val balanceIter = getBalance(it)
-                            ret.publicAccounts.add(
-                                PublicAccountInfo(
-                                    balance = 
"${demobank.config.currency}:$balanceIter",
-                                    iban = it.iban,
-                                    accountLabel = it.label
-                                )
-                            )
-                        }
-                    }
-                    call.respond(ret)
-                    return@get
-                }
-                delete("accounts/{account_name}") {
-                    val username = call.request.basicAuth()
-                    val demobank = ensureDemobank(call)
-                    val authGranted = !WITH_AUTH || username == "admin"
-                    val bankAccountLabel = 
call.expectUriComponent("account_name")
-                    /**
-                     * This helper fails if the demobank that is mentioned in 
the URI
-                     * is not hosting the account to be deleted.
-                     */
-                    val bankAccount = getBankAccountFromLabel(
-                        bankAccountLabel,
-                        demobank
-                    )
-                    if (!authGranted && username != bankAccount.owner)
-                        throw unauthorized("User '$username' has no rights to 
delete bank account '$bankAccountLabel'")
-                    transaction {
-                        val customerAccount = getCustomer(bankAccount.owner)
-                        bankAccount.delete()
-                        customerAccount.delete()
-                    }
-                    call.respond(object {})
-                    return@delete
-                }
-                // Keeping the prefix "testing" not to break tests.
-                post("/testing/register") {
-                    // Check demobank was created.
-                    val demobank = ensureDemobank(call)
-                    if (!demobank.config.allowRegistrations) {
-                        throw SandboxError(
-                            HttpStatusCode.UnprocessableEntity,
-                            "The bank doesn't allow new registrations at the 
moment."
-                        )
-                    }
-                    val req = call.receive<CustomerRegistration>()
-                    val newAccount = insertNewAccount(
-                        req.username,
-                        req.password,
-                        name = req.name,
-                        iban = req.iban,
-                        demobank = demobank.name,
-                        isPublic = req.isPublic
-                    )
-                    val balance = getBalance(newAccount.bankAccount)
-                    call.respond(object {
-                        val balance = getBalanceForJson(balance, 
demobank.config.currency)
-                        val paytoUri = buildIbanPaytoUri(
-                            iban = newAccount.bankAccount.iban,
-                            bic = newAccount.bankAccount.bic,
-                            receiverName = 
getPersonNameFromCustomer(req.username)
-                        )
-                        val iban = newAccount.bankAccount.iban
-                        val debitThreshold = getMaxDebitForUser(
-                            req.username,
-                            demobank.name
-                        ).toString()
-                    })
-                    return@post
-                }
-            }
-            route("/ebics") {
-                /**
-                 * Associate an existing bank account to one EBICS subscriber.
-                 * If the subscriber is not found, it is created.
-                 */
-                post("/subscribers") {
-                    // Only the admin can create Ebics subscribers.
-                    val user = call.request.basicAuth()
-                    if (WITH_AUTH && (user != "admin")) throw forbidden("Only 
the Administrator can create Ebics subscribers.")
-                    val body = call.receive<EbicsSubscriberInfo>()
-                    // Create or get the Ebics subscriber that is found.
-                    transaction {
-                        // Check that host ID exists
-                        EbicsHostEntity.find {
-                            EbicsHostsTable.hostID eq body.hostID
-                        }.firstOrNull() ?: throw notFound("Host ID 
${body.hostID} not found.")
-                        val subscriber: EbicsSubscriberEntity = 
EbicsSubscriberEntity.find {
-                            (EbicsSubscribersTable.partnerId eq 
body.partnerID).and(
-                                EbicsSubscribersTable.userId eq body.userID
-                            ).and(EbicsSubscribersTable.hostId eq body.hostID)
-                        }.firstOrNull() ?: EbicsSubscriberEntity.new {
-                            partnerId = body.partnerID
-                            userId = body.userID
-                            systemId = null
-                            hostId = body.hostID
-                            state = SubscriberState.NEW
-                            nextOrderID = 1
-                        }
-                        val bankAccount = getBankAccountFromLabel(
-                            body.demobankAccountLabel,
-                            ensureDemobank(call)
-                        )
-                        subscriber.bankAccount = bankAccount
-                    }
-                    call.respond(object {})
-                    return@post
-                }
-            }
-        }
     }
-}
+}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/XMLEbicsConverter.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/XMLEbicsConverter.kt
deleted file mode 100644
index 2475ff53..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/XMLEbicsConverter.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package tech.libeufin.bank
-
-import io.ktor.http.*
-import io.ktor.http.content.*
-import io.ktor.serialization.*
-import io.ktor.util.reflect.*
-import io.ktor.utils.io.*
-import io.ktor.utils.io.charsets.*
-import io.ktor.utils.io.jvm.javaio.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import tech.libeufin.util.XMLUtil
-
-class XMLEbicsConverter : ContentConverter {
-    override suspend fun deserialize(
-        charset: Charset,
-        typeInfo: TypeInfo,
-        content: ByteReadChannel
-    ): Any {
-        return withContext(Dispatchers.IO) {
-            try {
-                
receiveEbicsXmlInternal(content.toInputStream().reader().readText())
-            } catch (e: Exception) {
-                throw SandboxError(
-                    HttpStatusCode.BadRequest,
-                    "Document is invalid XML."
-                )
-            }
-        }
-    }
-
-    // The following annotation was suggested by Intellij.
-    @Deprecated(
-        "Please override and use serializeNullable instead",
-        replaceWith = ReplaceWith("serializeNullable(charset, typeInfo, 
contentType, value)"),
-        level = DeprecationLevel.WARNING
-    )
-    override suspend fun serialize(
-        contentType: ContentType,
-        charset: Charset,
-        typeInfo: TypeInfo,
-        value: Any
-    ): OutgoingContent? {
-        return super.serializeNullable(contentType, charset, typeInfo, value)
-    }
-
-    override suspend fun serializeNullable(
-        contentType: ContentType,
-        charset: Charset,
-        typeInfo: TypeInfo,
-        value: Any?
-    ): OutgoingContent? {
-        val conv = try {
-            XMLUtil.convertJaxbToString(value)
-        } catch (e: Exception) {
-            /**
-             * Not always an error: the content negotiation might have
-             * only checked if this handler could convert the response.
-             */
-            return null
-        }
-        return OutputStreamContent({
-            val out = this;
-            withContext(Dispatchers.IO) {
-                out.write(conv.toByteArray())
-            }},
-            contentType.withCharset(charset)
-        )
-    }
-}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/bankAccount.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/bankAccount.kt
deleted file mode 100644
index 04812192..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/bankAccount.kt
+++ /dev/null
@@ -1,276 +0,0 @@
-package tech.libeufin.bank
-
-import io.ktor.http.*
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.util.*
-import java.math.BigDecimal
-
-/**
- * Check whether the given bank account would surpass the
- * debit threshold, in case the potential amount gets transferred.
- * Returns true when the debit WOULD be surpassed.  */
-fun maybeDebit(
-    accountLabel: String,
-    requestedAmount: BigDecimal,
-    demobankName: String = "default"
-): Boolean {
-    val demobank = getDemobank(demobankName) ?: throw notFound(
-        "Demobank '${demobankName}' not found when trying to check the debit 
threshold" +
-                " for user $accountLabel"
-    )
-    val balance = getBalance(accountLabel, demobankName)
-    val maxDebt = if (accountLabel == "admin") {
-        demobank.config.bankDebtLimit
-    } else demobank.config.usersDebtLimit
-    val balanceCheck = balance - requestedAmount
-    if (balanceCheck < BigDecimal.ZERO && balanceCheck.abs() > 
BigDecimal.valueOf(maxDebt.toLong())) {
-        logger.warn("User '$accountLabel' would surpass the debit" +
-                " threshold of $maxDebt, given the requested amount of 
${requestedAmount.toPlainString()}")
-        return true
-    }
-    return false
-}
-
-fun getMaxDebitForUser(
-    username: String,
-    demobankName: String = "default"
-): Int {
-    val bank = getDemobank(demobankName) ?: throw internalServerError(
-        "demobank $demobankName not found"
-    )
-    if (username == "admin") return bank.config.bankDebtLimit
-    return bank.config.usersDebtLimit
-}
-
-fun getBalanceForJson(value: BigDecimal, currency: String): BalanceJson {
-    return BalanceJson(
-        amount = "${currency}:${value.abs()}",
-        credit_debit_indicator = if (value < BigDecimal.ZERO) "debit" else 
"credit"
-    )
-}
-
-fun getBalance(bankAccount: BankAccountEntity): BigDecimal {
-    return BigDecimal(bankAccount.balance)
-}
-
-/**
- * This function balances _in bank account statements_.  A statement
- * witnesses the bank account after a given business time slot.  Therefore
- * _this_ type of balance is not guaranteed to hold the _actual_ and
- * more up-to-date bank account.  It'll be used when Sandbox will support
- * the issuing of bank statement.
- */
-fun getBalanceForStatement(
-    bankAccount: BankAccountEntity,
-    withPending: Boolean = true
-): BigDecimal {
-    val lastStatement = transaction {
-        BankAccountStatementEntity.find {
-            BankAccountStatementsTable.bankAccount eq bankAccount.id
-        }.lastOrNull()
-    }
-    var lastBalance = if (lastStatement == null) {
-        BigDecimal.ZERO
-    } else { BigDecimal(lastStatement.balanceClbd) }
-    if (!withPending) return lastBalance
-    /**
-     * Caller asks to include the pending transactions in the
-     * balance.  The block below gets the transactions happened
-     * later than the last statement and adds them to the balance
-     * that was calculated so far.
-     */
-    transaction {
-        val pendingTransactions = BankAccountTransactionEntity.find {
-            BankAccountTransactionsTable.account eq bankAccount.id and (
-                    
BankAccountTransactionsTable.date.greater(lastStatement?.creationTime ?: 0L))
-        }
-        pendingTransactions.forEach { tx ->
-            when (tx.direction) {
-                "DBIT" -> lastBalance -= parseDecimal(tx.amount)
-                "CRDT" -> lastBalance += parseDecimal(tx.amount)
-                else -> {
-                    logger.error("Transaction ${tx.id} is neither debit nor 
credit.")
-                    throw SandboxError(
-                        HttpStatusCode.InternalServerError,
-                        "Error in transactions state."
-                    )
-                }
-            }
-        }
-    }
-    return lastBalance
-}
-
-// Gets the balance of 'accountLabel', which is hosted at 'demobankName'.
-fun getBalance(accountLabel: String,
-               demobankName: String = "default"
-): BigDecimal {
-    val demobank = getDemobank(demobankName) ?: throw SandboxError(
-        HttpStatusCode.InternalServerError,
-        "Demobank '$demobankName' not found"
-    )
-
-    /**
-     * Setting withBankFault to true for the following reason:
-     * when asking for a balance, the bank should have made sure
-     * that the user has a bank account (together with a customer profile).
-     * If that's not the case, it's bank's fault, since it didn't check
-     * earlier.
-     */
-    val account = getBankAccountFromLabel(
-        accountLabel,
-        demobank,
-        withBankFault = true
-    )
-    return getBalance(account)
-}
-
-/**
- * 'debitAccount' and 'creditAccount' are customer usernames
- * and ALSO labels of the bank accounts owned by them.  They are
- * used to both resort a bank account and the legal name owning
- * the bank accounts.
- */
-fun wireTransfer(
-    debitAccount: String,
-    creditAccount: String,
-    demobank: String = "default",
-    subject: String,
-    amount: String, // $currency:x.y
-    pmtInfId: String? = null,
-    endToEndId: String? = null
-): String {
-    logger.debug("Maybe wire transfer (endToEndId: $endToEndId): $debitAccount 
-> $creditAccount, $subject, $amount")
-    return transaction {
-        val demobankDb = ensureDemobank(demobank)
-        val debitAccountDb = getBankAccountFromLabel(debitAccount, demobankDb)
-        val creditAccountDb = getBankAccountFromLabel(creditAccount, 
demobankDb)
-        val parsedAmount = parseAmount(amount)
-        // Potential amount to transfer.
-        val amountAsNumber = BigDecimal(parsedAmount.amount)
-        if (amountAsNumber == BigDecimal.ZERO)
-            throw badRequest("Wire transfers of zero not possible.")
-        if (parsedAmount.currency != demobankDb.config.currency)
-            throw badRequest(
-                "Won't wire transfer with currency: ${parsedAmount.currency}." 
+
-                        "  Only ${demobankDb.config.currency} allowed."
-            )
-        // Check funds are sufficient.
-        if (
-            maybeDebit(
-                debitAccountDb.label,
-                amountAsNumber,
-                demobankDb.name
-            )) {
-            logger.error("Account ${debitAccountDb.label} would surpass debit 
threshold.  Rollback wire transfer")
-            throw SandboxError(HttpStatusCode.Conflict, "Insufficient funds")
-        }
-        val timeStamp = getNowMillis()
-        val transactionRef = getRandomString(8)
-        BankAccountTransactionEntity.new {
-            creditorIban = creditAccountDb.iban
-            creditorBic = creditAccountDb.bic
-            this.creditorName = 
getPersonNameFromCustomer(creditAccountDb.owner)
-            debtorIban = debitAccountDb.iban
-            debtorBic = debitAccountDb.bic
-            debtorName = getPersonNameFromCustomer(debitAccountDb.owner)
-            this.subject = subject
-            this.amount = parsedAmount.amount
-            this.currency = demobankDb.config.currency
-            date = timeStamp
-            accountServicerReference = transactionRef
-            account = creditAccountDb
-            direction = "CRDT"
-            this.demobank = demobankDb
-            this.pmtInfId = pmtInfId
-        }
-        BankAccountTransactionEntity.new {
-            creditorIban = creditAccountDb.iban
-            creditorBic = creditAccountDb.bic
-            this.creditorName = 
getPersonNameFromCustomer(creditAccountDb.owner)
-            debtorIban = debitAccountDb.iban
-            debtorBic = debitAccountDb.bic
-            debtorName = getPersonNameFromCustomer(debitAccountDb.owner)
-            this.subject = subject
-            this.amount = parsedAmount.amount
-            this.currency = demobankDb.config.currency
-            date = timeStamp
-            accountServicerReference = transactionRef
-            account = debitAccountDb
-            direction = "DBIT"
-            this.demobank = demobankDb
-            this.pmtInfId = pmtInfId
-            this.endToEndId = endToEndId
-        }
-
-        // Adjusting the balances (acceptable debit conditions checked before).
-        // Debit:
-        val newDebitBalance = (BigDecimal(debitAccountDb.balance) - 
amountAsNumber).roundToTwoDigits()
-        debitAccountDb.balance = newDebitBalance.toPlainString()
-        // Credit:
-        val newCreditBalance = (BigDecimal(creditAccountDb.balance) + 
amountAsNumber).roundToTwoDigits()
-        creditAccountDb.balance = newCreditBalance.toPlainString()
-
-        // Signaling this wire transfer's event.
-        if (this.isPostgres()) {
-            val creditChannel = buildChannelName(
-                NotificationsChannelDomains.LIBEUFIN_REGIO_TX,
-                creditAccountDb.label
-            )
-            this.postgresNotify(creditChannel, "CRDT")
-            val debitChannel = buildChannelName(
-                NotificationsChannelDomains.LIBEUFIN_REGIO_TX,
-                debitAccountDb.label
-            )
-            this.postgresNotify(debitChannel, "DBIT")
-        }
-        transactionRef
-    }
-}
-
-/**
- * Helper that constructs a transactions history page
- * according to the URI parameters passed to Access API's
- * GET /transactions.
- */
-data class HistoryParams(
-    val pageNumber: Int,
-    val pageSize: Int,
-    val fromMs: Long,
-    val untilMs: Long,
-    val bankAccount: BankAccountEntity
-)
-
-fun extractTxHistory(params: HistoryParams): List<XLibeufinBankTransaction> {
-    val ret = mutableListOf<XLibeufinBankTransaction>()
-
-    /**
-     * Helper that gets transactions earlier than the 'firstElementId'
-     * transaction AND that match the URI parameters.
-     */
-    fun getPage(firstElementId: Long): Iterable<BankAccountTransactionEntity> {
-        return BankAccountTransactionEntity.find {
-            (BankAccountTransactionsTable.id lessEq firstElementId) and
-                    (BankAccountTransactionsTable.account eq 
params.bankAccount.id) and
-                    (BankAccountTransactionsTable.date.between(params.fromMs, 
params.untilMs))
-        }.sortedByDescending { it.id.value }.take(params.pageSize)
-    }
-    // Gets a pointer to the last transaction of this bank account.
-    val lastTransaction: BankAccountTransactionEntity? = 
params.bankAccount.lastTransaction
-    if (lastTransaction == null) return ret
-    var nextPageIdUpperLimit: Long = lastTransaction.id.value
-
-    // This loop fetches (and discards) pages until the desired one is found.
-    for (i in 1..(params.pageNumber)) {
-        val pageBuf = getPage(nextPageIdUpperLimit)
-        logger.debug("pageBuf #$i follows.  Request wants 
#${params.pageNumber}:")
-        pageBuf.forEach { logger.debug("ID: ${it.id}, subject: ${it.subject}, 
amount: ${it.currency}:${it.amount}") }
-        if (pageBuf.none()) return ret
-        nextPageIdUpperLimit = pageBuf.last().id.value - 1
-        if (i == params.pageNumber) pageBuf.forEach {
-            ret.add(getHistoryElementFromTransactionRow(it))
-        }
-    }
-    return ret
-}
\ No newline at end of file
diff --git a/bank/src/main/resources/logback.xml 
b/bank/src/main/resources/logback.xml
index cefb7182..d04f095e 100644
--- a/bank/src/main/resources/logback.xml
+++ b/bank/src/main/resources/logback.xml
@@ -6,7 +6,7 @@
         </encoder>
     </appender>
 
-    <logger name="tech.libeufin.sandbox" level="ALL"  additivity="false">
+    <logger name="tech.libeufin.bank" level="ALL"  additivity="false">
         <appender-ref ref="STDERR" />
     </logger>
     <logger name="tech.libeufin.util" level="ALL"  additivity="false">
diff --git a/bank/src/test/kotlin/BalanceTest.kt 
b/bank/src/test/kotlin/BalanceTest.kt
deleted file mode 100644
index eb09cc64..00000000
--- a/bank/src/test/kotlin/BalanceTest.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-import org.jetbrains.exposed.sql.SchemaUtils
-import org.jetbrains.exposed.sql.insert
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Test
-import tech.libeufin.sandbox.*
-import tech.libeufin.util.millis
-import tech.libeufin.util.roundToTwoDigits
-import java.math.BigDecimal
-import java.time.LocalDateTime
-
-class BalanceTest {
-    @Test
-    fun balanceTest() {
-        val config = DemobankConfig(
-            currency = "EUR",
-            bankDebtLimit = 1000000,
-            usersDebtLimit = 10000,
-            allowRegistrations = true,
-            demobankName = "default",
-            withSignupBonus = false
-        )
-        withTestDatabase {
-            transaction {
-                insertConfigPairs(config)
-                val demobank = DemobankConfigEntity.new {
-                    name = "default"
-                }
-                val one = BankAccountEntity.new {
-                    iban = "IBAN 1"
-                    bic = "BIC"
-                    label = "label 1"
-                    owner = "admin"
-                    this.demoBank = demobank
-                }
-                val other = BankAccountEntity.new {
-                    iban = "IBAN 2"
-                    bic = "BIC"
-                    label = "label 2"
-                    owner = "admin"
-                    this.demoBank = demobank
-                }
-                BankAccountTransactionEntity.new {
-                    account = one
-                    creditorIban = "earns"
-                    creditorBic = "BIC"
-                    creditorName = "Creditor Name"
-                    debtorIban = "spends"
-                    debtorBic = "BIC"
-                    debtorName = "Debitor Name"
-                    subject = "deal"
-                    amount = "1"
-                    date = LocalDateTime.now().millis()
-                    currency = "EUR"
-                    pmtInfId = "0"
-                    direction = "CRDT"
-                    accountServicerReference = 
"test-account-servicer-reference"
-                    this.demobank = demobank
-                }
-                BankAccountTransactionEntity.new {
-                    account = one
-                    creditorIban = "earns"
-                    creditorBic = "BIC"
-                    creditorName = "Creditor Name"
-                    debtorIban = "spends"
-                    debtorBic = "BIC"
-                    debtorName = "Debitor Name"
-                    subject = "deal"
-                    amount = "1"
-                    date = LocalDateTime.now().millis()
-                    currency = "EUR"
-                    pmtInfId = "0"
-                    direction = "CRDT"
-                    accountServicerReference = 
"test-account-servicer-reference"
-                    this.demobank = demobank
-                }
-                BankAccountTransactionEntity.new {
-                    account = one
-                    creditorIban = "earns"
-                    creditorBic = "BIC"
-                    creditorName = "Creditor Name"
-                    debtorIban = "spends"
-                    debtorBic = "BIC"
-                    debtorName = "Debitor Name"
-                    subject = "deal"
-                    amount = "1"
-                    date = LocalDateTime.now().millis()
-                    currency = "EUR"
-                    pmtInfId = "0"
-                    direction = "DBIT"
-                    accountServicerReference = 
"test-account-servicer-reference"
-                    this.demobank = demobank
-                }
-                wireTransfer(
-                    other.label, one.label, demobank.name, "one gets 1", 
"EUR:1"
-                )
-                wireTransfer(
-                    other.label, one.label, demobank.name, "one gets another 
1", "EUR:1"
-                )
-                wireTransfer(
-                    one.label, other.label, demobank.name, "one gives 1", 
"EUR:1"
-                )
-                val maybeOneBalance: BigDecimal = getBalance(one)
-                println(maybeOneBalance)
-                assert(BigDecimal.ONE.roundToTwoDigits() == 
maybeOneBalance.roundToTwoDigits())
-            }
-        }
-    }
-    @Test
-    fun balanceAbsTest() {
-        val minus = BigDecimal.ZERO - BigDecimal.ONE
-        val plus = BigDecimal.ONE
-        println(minus.abs().toPlainString())
-        println(plus.abs().toPlainString())
-    }
-}
diff --git a/bank/src/test/kotlin/DBTest.kt b/bank/src/test/kotlin/DBTest.kt
deleted file mode 100644
index bc5a33c5..00000000
--- a/bank/src/test/kotlin/DBTest.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2020 Taler Systems S.A.
- *
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
- *
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
- * Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING.  If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Test
-import tech.libeufin.sandbox.*
-import tech.libeufin.util.connectWithSchema
-import tech.libeufin.util.getCurrentUser
-import tech.libeufin.util.getJdbcConnectionFromPg
-import tech.libeufin.util.millis
-import java.io.File
-import java.time.LocalDateTime
-import kotlin.reflect.KProperty
-import kotlin.reflect.typeOf
-
-/**
- * Run a block after connecting to the test database.
- * Cleans up the DB file afterwards.
- */
-fun withTestDatabase(f: () -> Unit) {
-    dbDropTables("postgresql:///libeufincheck")
-    dbCreateTables("postgresql:///libeufincheck")
-    f()
-}
-
-class DBTest {
-    private var config = DemobankConfig(
-        currency = "EUR",
-        bankDebtLimit = 1000000,
-        usersDebtLimit = 10000,
-        allowRegistrations = true,
-        demobankName = "default",
-        withSignupBonus = false,
-    )
-
-    /**
-     * This tests the conversion from a Postgres connection
-     * string to a JDBC one.
-     */
-    @Test
-    fun connectionStringTest() {
-        getJdbcConnectionFromPg("postgres://auditor-basedb")
-        var conv = getJdbcConnectionFromPg("postgresql:///libeufincheck")
-        connectWithSchema(getJdbcConnectionFromPg("postgres:///libeufincheck"))
-        connectWithSchema(conv)
-        conv = 
getJdbcConnectionFromPg("postgresql://localhost:5432/libeufincheck?user=${System.getProperty("user.name")}")
-        connectWithSchema(conv)
-        conv = 
getJdbcConnectionFromPg("postgresql:///libeufincheck?host=/tmp/libeufin")
-        var exception: Exception? = null
-        try {
-            connectWithSchema(conv)
-        } catch (e: Exception) {
-            exception = e
-        }
-        assert(exception is UtilError)
-    }
-
-    /**
-     * Storing configuration values into the database,
-     * then extract them and check that they equal the
-     * configuration model object.
-     */
-    @Test
-    fun insertPairsTest() {
-        withTestDatabase {
-            // Config model.
-            val config = DemobankConfig(
-                currency = "EUR",
-                bankDebtLimit = 1,
-                usersDebtLimit = 2,
-                allowRegistrations = true,
-                demobankName = "default",
-                withSignupBonus = true
-            )
-            transaction {
-                DemobankConfigEntity.new { name = "default" }
-                insertConfigPairs(config)
-                val db = getDefaultDemobank()
-                /**
-                 * db.config extracts config values from the database
-                 * and puts them in a fresh config model object.
-                 */
-                assert(config.hashCode() == db.config.hashCode())
-            }
-        }
-    }
-
-    @Test
-    fun betweenDates() {
-        withTestDatabase {
-            transaction {
-                insertConfigPairs(config)
-                val demobank = DemobankConfigEntity.new {
-                    name = "default"
-                }
-                val bankAccount = BankAccountEntity.new {
-                    iban = "iban"
-                    bic = "bic"
-                    label = "label"
-                    owner = "test"
-                    demoBank = demobank
-                }
-                BankAccountTransactionEntity.new {
-                    account = bankAccount
-                    creditorIban = "earns"
-                    creditorBic = "BIC"
-                    creditorName = "Creditor Name"
-                    debtorIban = "spends"
-                    debtorBic = "BIC"
-                    debtorName = "Debitor Name"
-                    subject = "deal"
-                    amount = "EUR:1"
-                    date = LocalDateTime.now().millis()
-                    currency = "EUR"
-                    pmtInfId = "0"
-                    direction = "DBIT"
-                    accountServicerReference = 
"test-account-servicer-reference"
-                    this.demobank = demobank
-                }
-            }
-            // The block below tests the date range in the database query
-            transaction {
-                addLogger(StdOutSqlLogger)
-                BankAccountTransactionEntity.find {
-                    BankAccountTransactionsTable.date.between(
-                        0, // 1970-01-01
-                        LocalDateTime.now().millis() //
-                    )
-                }.apply {
-                    assert(this.count() == 1L)
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/DatabaseTest.kt 
b/bank/src/test/kotlin/DatabaseTest.kt
index 7716cd26..cb4755db 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -1,6 +1,9 @@
 import org.junit.Test
-import tech.libeufin.sandbox.*
+import tech.libeufin.bank.*
 import tech.libeufin.util.execCommand
+import tech.libeufin.util.getNow
+import tech.libeufin.util.toMicro
+import java.util.Random
 import java.util.UUID
 
 class DatabaseTest {
@@ -23,20 +26,18 @@ class DatabaseTest {
         cashoutCurrency = "KUDOS"
     )
     private val bankAccountFoo = BankAccount(
-        iban = "FOO-IBAN-XYZ",
-        bic = "FOO-BIC",
-        bankAccountLabel = "foo",
+        internalPaytoUri = "FOO-IBAN-XYZ",
         lastNexusFetchRowId = 1L,
         owningCustomerId = 1L,
-        hasDebt = false
+        hasDebt = false,
+        maxDebt = TalerAmount(10, 1)
     )
     private val bankAccountBar = BankAccount(
-        iban = "BAR-IBAN-ABC",
-        bic = "BAR-BIC",
-        bankAccountLabel = "bar",
+        internalPaytoUri = "BAR-IBAN-ABC",
         lastNexusFetchRowId = 1L,
         owningCustomerId = 2L,
-        hasDebt = false
+        hasDebt = false,
+        maxDebt = TalerAmount(10, 1)
     )
 
     fun initDb(): Database {
@@ -53,22 +54,41 @@ class DatabaseTest {
         return db
     }
 
+    @Test
+    fun bearerTokenTest() {
+        val db = initDb()
+        val tokenBytes = ByteArray(32)
+        Random().nextBytes(tokenBytes)
+        val token = BearerToken(
+            bankCustomer = 1L,
+            content = tokenBytes,
+            creationTime = getNow().toMicro(), // make .toMicro()? implicit?
+            expirationTime = getNow().plusDays(1).toMicro(),
+            scope = TokenScope.readonly
+        )
+        assert(db.bearerTokenGet(token.content) == null)
+        db.customerCreate(customerBar) // Tokens need owners.
+        assert(db.bearerTokenCreate(token))
+        assert(db.bearerTokenGet(tokenBytes) != null)
+    }
     @Test
     fun bankTransactionsTest() {
         val db = initDb()
-        assert(db.customerCreate(customerFoo))
-        assert(db.customerCreate(customerBar))
+        val fooId = db.customerCreate(customerFoo)
+        assert(fooId != null)
+        val barId = db.customerCreate(customerBar)
+        assert(barId != null)
         assert(db.bankAccountCreate(bankAccountFoo))
         assert(db.bankAccountCreate(bankAccountBar))
-        var fooAccount = db.bankAccountGetFromLabel("foo")
+        var fooAccount = db.bankAccountGetFromOwnerId(fooId!!)
         assert(fooAccount?.hasDebt == false) // Foo has NO debit.
         // Preparing the payment data.
         db.bankAccountSetMaxDebt(
-            "foo",
+            fooId,
             TalerAmount(100, 0)
         )
         db.bankAccountSetMaxDebt(
-            "bar",
+            barId!!,
             TalerAmount(50, 0)
         )
         val fooPaysBar = BankInternalTransaction(
@@ -83,13 +103,13 @@ class DatabaseTest {
         )
         val firstSpending = db.bankTransactionCreate(fooPaysBar) // Foo pays 
Bar and goes debit.
         assert(firstSpending == Database.BankTransactionResult.SUCCESS)
-        fooAccount = db.bankAccountGetFromLabel("foo")
+        fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Foo: credit -> debit
         assert(fooAccount?.hasDebt == true) // Asserting Foo's debit.
         // Now checking that more spending doesn't get Foo out of debit.
         val secondSpending = db.bankTransactionCreate(fooPaysBar)
         assert(secondSpending == Database.BankTransactionResult.SUCCESS)
-        fooAccount = db.bankAccountGetFromLabel("foo")
+        fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Checking that Foo's debit is two times the paid amount
         // Foo: debit -> debit
         assert(fooAccount?.balance?.value == 20L
@@ -97,7 +117,7 @@ class DatabaseTest {
                 && fooAccount.hasDebt
         )
         // Asserting Bar has a positive balance and what Foo paid so far.
-        var barAccount = db.bankAccountGetFromLabel("bar")
+        var barAccount = db.bankAccountGetFromOwnerId(barId)
         val barBalance: TalerAmount? = barAccount?.balance
         assert(
             barAccount?.hasDebt == false
@@ -116,7 +136,7 @@ class DatabaseTest {
         )
         val barPays = db.bankTransactionCreate(barPaysFoo)
         assert(barPays == Database.BankTransactionResult.SUCCESS)
-        barAccount = db.bankAccountGetFromLabel("bar")
+        barAccount = db.bankAccountGetFromOwnerId(barId)
         val barBalanceTen: TalerAmount? = barAccount?.balance
         // Bar: credit -> credit
         assert(barAccount?.hasDebt == false && barBalanceTen?.value == 10L && 
barBalanceTen.frac == 0)
@@ -124,8 +144,8 @@ class DatabaseTest {
         val barPaysAgain = db.bankTransactionCreate(barPaysFoo)
         assert(barPaysAgain == Database.BankTransactionResult.SUCCESS)
         // Refreshing the two accounts.
-        barAccount = db.bankAccountGetFromLabel("bar")
-        fooAccount = db.bankAccountGetFromLabel("foo")
+        barAccount = db.bankAccountGetFromOwnerId(barId)
+        fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Foo should have returned to zero and no debt, same for Bar.
         // Foo: debit -> credit
         assert(fooAccount?.hasDebt == false && barAccount?.hasDebt == false)
@@ -134,8 +154,8 @@ class DatabaseTest {
         // Bringing Bar to debit.
         val barPaysMore = db.bankTransactionCreate(barPaysFoo)
         assert(barPaysAgain == Database.BankTransactionResult.SUCCESS)
-        barAccount = db.bankAccountGetFromLabel("bar")
-        fooAccount = db.bankAccountGetFromLabel("foo")
+        barAccount = db.bankAccountGetFromOwnerId(barId)
+        fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Bar: credit -> debit
         assert(fooAccount?.hasDebt == false && barAccount?.hasDebt == true)
         assert(fooAccount?.balance?.equals(TalerAmount(10, 0)) == true)
@@ -148,7 +168,7 @@ class DatabaseTest {
         db.customerCreate(customerFoo)
         assert(db.customerGetFromLogin("foo")?.name == "Foo")
         // Trigger conflict.
-        assert(!db.customerCreate(customerFoo))
+        assert(db.customerCreate(customerFoo) == null)
     }
     @Test
     fun configTest() {
@@ -161,19 +181,18 @@ class DatabaseTest {
     @Test
     fun bankAccountTest() {
         val db = initDb()
-        assert(db.bankAccountGetFromLabel("foo") == null)
-        assert(db.customerCreate(customerFoo))
+        assert(db.bankAccountGetFromOwnerId(1L) == null)
+        assert(db.customerCreate(customerFoo) != null)
         assert(db.bankAccountCreate(bankAccountFoo))
         assert(!db.bankAccountCreate(bankAccountFoo)) // Triggers conflict.
-        assert(db.bankAccountGetFromLabel("foo")?.bankAccountLabel == "foo")
-        
assert(db.bankAccountGetFromLabel("foo")?.balance?.equals(TalerAmount(0, 0)) == 
true)
+        
assert(db.bankAccountGetFromOwnerId(1L)?.balance?.equals(TalerAmount(0, 0)) == 
true)
     }
 
     @Test
     fun withdrawalTest() {
         val db = initDb()
         val uuid = UUID.randomUUID()
-        assert(db.customerCreate(customerFoo))
+        assert(db.customerCreate(customerFoo) != null)
         assert(db.bankAccountCreate(bankAccountFoo))
         // insert new.
         assert(db.talerWithdrawalCreate(
@@ -220,16 +239,17 @@ class DatabaseTest {
             buyInFee = TalerAmount(0, 22),
             sellAtRatio = 2,
             sellOutFee = TalerAmount(0, 44),
-            cashoutAddress = "IBAN",
+            credit_payto_uri = "IBAN",
             cashoutCurrency = "KUDOS",
             creationTime = 3L,
             subject = "31st",
             tanChannel = TanChannel.sms,
             tanCode = "secret",
         )
-        assert(db.customerCreate(customerFoo))
+        val fooId = db.customerCreate(customerFoo)
+        assert(fooId != null)
         assert(db.bankAccountCreate(bankAccountFoo))
-        assert(db.customerCreate(customerBar))
+        assert(db.customerCreate(customerBar) != null)
         assert(db.bankAccountCreate(bankAccountBar))
         assert(db.cashoutCreate(op))
         val fromDb = db.cashoutGetFromUuid(op.cashoutUuid)
@@ -237,10 +257,11 @@ class DatabaseTest {
         assert(db.cashoutDelete(op.cashoutUuid) == 
Database.CashoutDeleteResult.SUCCESS)
         assert(db.cashoutCreate(op))
         db.bankAccountSetMaxDebt(
-            "foo",
+            fooId!!,
             TalerAmount(100, 0)
         )
-        assert(db.bankTransactionCreate(BankInternalTransaction(
+        assert(db.bankTransactionCreate(
+            BankInternalTransaction(
             creditorAccountId = 2,
             debtorAccountId = 1,
             subject = "backing the cash-out",
@@ -249,7 +270,8 @@ class DatabaseTest {
             endToEndId = "end-to-end-id",
             paymentInformationId = "pmtinfid",
             transactionDate = 100000L
-        )) == Database.BankTransactionResult.SUCCESS)
+        )
+        ) == Database.BankTransactionResult.SUCCESS)
         // Confirming the cash-out
         assert(db.cashoutConfirm(op.cashoutUuid, 1L, 1L))
         // Checking the confirmation took place.
diff --git a/bank/src/test/kotlin/EbicsErrorTest.kt 
b/bank/src/test/kotlin/EbicsErrorTest.kt
deleted file mode 100644
index e0be736b..00000000
--- a/bank/src/test/kotlin/EbicsErrorTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-import org.apache.xml.security.binding.xmldsig.SignatureType
-import org.junit.Test
-import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.XMLUtil
-import tech.libeufin.util.ebics_h004.EbicsResponse
-import tech.libeufin.util.ebics_h004.EbicsTypes
-
-class EbicsErrorTest {
-
-    @Test
-    fun makeEbicsErrorResponse() {
-        val pair = CryptoUtil.generateRsaKeyPair(2048)
-        val resp = EbicsResponse.createForUploadWithError(
-            "[EBICS_ERROR] abc",
-            "012345",
-            EbicsTypes.TransactionPhaseType.INITIALISATION
-        )
-        val signedResp = XMLUtil.signEbicsResponse(resp, pair.private)
-        XMLUtil.validateFromString(signedResp)
-        assert(resp.header.mutable.reportText == "[EBICS_ERROR] abc")
-        assert(resp.header.mutable.returnCode == "012345")
-        assert(resp.body.returnCode.value == "012345")
-    }
-}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt 
b/bank/src/test/kotlin/LibeuFinApiTest.kt
new file mode 100644
index 00000000..c292d796
--- /dev/null
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -0,0 +1,26 @@
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.http.*
+import io.ktor.server.testing.*
+import org.junit.Test
+import tech.libeufin.bank.Database
+import tech.libeufin.bank.webApp
+
+class LibeuFinApiTest {
+    @Test
+    fun createAccountTest() {
+        testApplication {
+            System.setProperty(
+                "BANK_DB_CONNECTION_STRING",
+                "jdbc:postgresql:///libeufincheck"
+            )
+            val db = Database("jdbc:postgresql:///libeufincheck")
+            db.configSet("max_debt_ordinary_customers", "KUDOS:11")
+            application(webApp)
+            client.post("/test-json") {
+                expectSuccess = true
+                contentType(ContentType.Application.Json)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/StringsTest.kt 
b/bank/src/test/kotlin/StringsTest.kt
deleted file mode 100644
index 892a419c..00000000
--- a/bank/src/test/kotlin/StringsTest.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-import org.junit.Test
-import tech.libeufin.util.hasWopidPlaceholder
-import tech.libeufin.util.validateBic
-
-class StringsTest {
-
-    @Test
-    fun hasWopidTest() {
-        assert(hasWopidPlaceholder("http://example.com/#/{wopid}";))
-        assert(!hasWopidPlaceholder("http://example.com";))
-        assert(hasWopidPlaceholder("http://example.com/#/{WOPID}";))
-        assert(!hasWopidPlaceholder("{ W O P I D }"))
-    }
-
-    @Test
-    fun replaceWopidPlaceholderTest() {
-        assert(
-            "http://example.com/#/operation/{wopid}".replace("{wopid}", "987")
-            == "http://example.com/#/operation/987";
-        )
-        assert("http://example.com".replace("{wopid}", "not-replaced")
-                == "http://example.com";
-        )
-    }
-
-    @Test
-    fun bicTest() {
-        assert(validateBic("GENODEM1GLS"))
-        assert(validateBic("AUTOATW1XXX"))
-    }
-
-    @Test
-    fun booleanToString() {
-        assert(true.toString() == "true")
-        assert(false.toString() == "false")
-    }
-}
\ No newline at end of file
diff --git a/database-versioning/new/libeufin-bank-0001.sql 
b/database-versioning/new/libeufin-bank-0001.sql
index 314c4ea2..9daaa7b0 100644
--- a/database-versioning/new/libeufin-bank-0001.sql
+++ b/database-versioning/new/libeufin-bank-0001.sql
@@ -32,6 +32,9 @@ COMMENT ON TYPE taler_amount
 CREATE TYPE direction_enum
   AS ENUM ('credit', 'debit');
 
+CREATE TYPE token_scope_enum
+  AS ENUM ('readonly', 'readwrite');
+
 CREATE TYPE tan_enum
   AS ENUM ('sms', 'email', 'file'); -- file is for testing purposes.
 
@@ -44,7 +47,6 @@ CREATE TYPE subscriber_key_state_enum
 CREATE TYPE subscriber_state_enum
   AS ENUM ('new', 'confirmed');
 
-
 -- FIXME: comments on types (see exchange for example)!
 
 -- start of: bank config tables.  FIXME: eventually replaced by the INI file.
@@ -65,30 +67,44 @@ CREATE TABLE IF NOT EXISTS customers
   ,name TEXT
   ,email TEXT
   ,phone TEXT
-  ,cashout_payto TEXT
+  ,cashout_payto TEXT -- here because has no business meaning inside 
libeufin-bank
   ,cashout_currency TEXT
   );
 
 COMMENT ON COLUMN customers.cashout_payto
   IS 'RFC 8905 payto URI to collect fiat payments that come from the 
conversion of regional currency cash-out operations.';
-
 COMMENT ON COLUMN customers.name
   IS 'Full name of the customer.';
 
+CREATE TABLE IF NOT EXISTS bearer_tokens
+  (bearer_token_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE
+  ,content BYTEA NOT NULL UNIQUE CHECK (LENGTH(content)=32)
+  ,creation_time INT8
+  ,expiration_time INT8
+  ,scope token_scope_enum
+  ,bank_customer BIGINT NOT NULL REFERENCES customers(customer_id) ON DELETE 
CASCADE
+);
+
+COMMENT ON TABLE bearer_tokens
+  IS 'Login tokens associated with one bank customer.  There is currently'
+     ' no garbage collector that deletes the expired tokens from the table';
+
+COMMENT ON COLUMN bearer_tokens.bank_customer
+  IS 'The customer that directly created this token, or the customer that'
+     ' created the very first token that originated all the refreshes until'
+     ' this token was created.';
 
 CREATE TABLE IF NOT EXISTS bank_accounts 
   (bank_account_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE
-  ,iban TEXT NOT NULL UNIQUE
-  ,bic TEXT NOT NULL
-  ,bank_account_label TEXT NOT NULL
-  ,owning_customer_id BIGINT NOT NULL
+  ,internal_payto_uri TEXT NOT NULL UNIQUE
+  ,owning_customer_id BIGINT NOT NULL UNIQUE -- UNIQUE enforces 1-1 map with 
customers
     REFERENCES customers(customer_id)
   ,is_public BOOLEAN DEFAULT FALSE NOT NULL -- privacy by default
+  ,is_taler_exchange BOOLEAN DEFAULT FALSE NOT NULL
   ,last_nexus_fetch_row_id BIGINT
   ,balance taler_amount DEFAULT (0, 0)
   ,max_debt taler_amount DEFAULT (0, 0)
   ,has_debt BOOLEAN NOT NULL DEFAULT FALSE
-  ,UNIQUE (owning_customer_id, bank_account_label)
   );
 
 COMMENT ON TABLE bank_accounts
@@ -110,9 +126,6 @@ COMMENT ON COLUMN bank_accounts.is_public
   IS 'Indicates whether the bank account history
 can be publicly shared';
 
-COMMENT ON COLUMN bank_accounts.bank_account_label
-  IS 'Label of the bank account';
-
 COMMENT ON COLUMN bank_accounts.owning_customer_id
   IS 'Login that owns the bank account';
 
@@ -122,11 +135,9 @@ COMMENT ON COLUMN bank_accounts.owning_customer_id
 
 CREATE TABLE IF NOT EXISTS bank_account_transactions 
   (bank_transaction_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE
-  ,creditor_iban TEXT NOT NULL
-  ,creditor_bic TEXT NULL
+  ,creditor_payto_uri TEXT NOT NULL
   ,creditor_name TEXT NOT NULL
-  ,debtor_iban TEXT NOT NULL
-  ,debtor_bic TEXT NULL
+  ,debtor_payto_uri TEXT NOT NULL
   ,debtor_name TEXT NOT NULL
   ,subject TEXT NOT NULL
   ,amount taler_amount NOT NULL
@@ -175,16 +186,14 @@ CREATE TABLE IF NOT EXISTS cashout_operations
     REFERENCES bank_accounts(bank_account_id)
     ON DELETE CASCADE
     ON UPDATE RESTRICT
-  ,cashout_address TEXT NOT NULL -- FIXME: clarify payto, if it's a payto use 
it in the name
-  ,cashout_currency TEXT NOT NULL
+  ,credit_payto_uri TEXT NOT NULL
+  ,cashout_currency TEXT NOT NULL -- need, or include in credit_payto_uri?
   );
 
 -- FIXME: table comment missing
 
 COMMENT ON COLUMN cashout_operations.tan_confirmation_time
   IS 'Timestamp when the customer confirmed the cash-out operation via TAN';
-COMMENT ON COLUMN cashout_operations.cashout_address
-  IS 'IBAN that ultimately gets the fiat payment';
 COMMENT ON COLUMN cashout_operations.tan_code
   IS 'text that the customer must send to confirm the cash-out operation';
 
diff --git a/database-versioning/new/procedures.sql 
b/database-versioning/new/procedures.sql
index 8fd3c8c1..c31aa5f1 100644
--- a/database-versioning/new/procedures.sql
+++ b/database-versioning/new/procedures.sql
@@ -104,11 +104,9 @@ AS $$
 DECLARE
 debtor_has_debt BOOLEAN;
 debtor_balance taler_amount;
-debtor_iban TEXT;
-debtor_bic TEXT;
+debtor_payto_uri TEXT;
 debtor_name TEXT;
-creditor_iban TEXT;
-creditor_bic TEXT;
+creditor_payto_uri TEXT;
 creditor_name TEXT;
 debtor_max_debt taler_amount;
 creditor_has_debt BOOLEAN;
@@ -128,12 +126,12 @@ SELECT
   has_debt,
   (balance).val, (balance).frac,
   (max_debt).val, (max_debt).frac,
-  iban, bic, customers.name
+  internal_payto_uri, customers.name
   INTO
     debtor_has_debt,
     debtor_balance.val, debtor_balance.frac,
     debtor_max_debt.val, debtor_max_debt.frac,
-    debtor_iban, debtor_bic, debtor_name 
+    debtor_payto_uri, debtor_name
   FROM bank_accounts
   JOIN customers ON (bank_accounts.owning_customer_id = customers.customer_id)
   WHERE bank_account_id=in_debtor_account_id;
@@ -148,11 +146,11 @@ out_nx_debtor=FALSE;
 SELECT
   has_debt,
   (balance).val, (balance).frac,
-  iban, bic, customers.name
+  internal_payto_uri, customers.name
   INTO
     creditor_has_debt,
     creditor_balance.val, creditor_balance.frac,
-    creditor_iban, creditor_bic, creditor_name 
+    creditor_payto_uri, creditor_name
   FROM bank_accounts
   JOIN customers ON (bank_accounts.owning_customer_id = customers.customer_id)
   WHERE bank_account_id=in_creditor_account_id;
@@ -251,11 +249,9 @@ out_balance_insufficient=FALSE;
 -- now actually create the bank transaction.
 -- debtor side:
 INSERT INTO bank_account_transactions (
-  creditor_iban
-  ,creditor_bic
+  creditor_payto_uri
   ,creditor_name
-  ,debtor_iban
-  ,debtor_bic
+  ,debtor_payto_uri
   ,debtor_name
   ,subject
   ,amount
@@ -267,11 +263,9 @@ INSERT INTO bank_account_transactions (
   ,bank_account_id
   )
 VALUES (
-  creditor_iban,
-  creditor_bic,
+  creditor_payto_uri,
   creditor_name,
-  debtor_iban,
-  debtor_bic,
+  debtor_payto_uri,
   debtor_name,
   in_subject,
   in_amount,
@@ -285,11 +279,9 @@ VALUES (
 
 -- debtor side:
 INSERT INTO bank_account_transactions (
-  creditor_iban
-  ,creditor_bic
+  creditor_payto_uri
   ,creditor_name
-  ,debtor_iban
-  ,debtor_bic
+  ,debtor_payto_uri
   ,debtor_name
   ,subject
   ,amount
@@ -301,11 +293,9 @@ INSERT INTO bank_account_transactions (
   ,bank_account_id
   )
 VALUES (
-  creditor_iban,
-  creditor_bic,
+  creditor_payto_uri,
   creditor_name,
-  debtor_iban,
-  debtor_bic,
+  debtor_payto_uri,
   debtor_name,
   in_subject,
   in_amount,
diff --git a/nexus/build.gradle b/nexus/build.gradle
index 13a2c806..66ec7f98 100644
--- a/nexus/build.gradle
+++ b/nexus/build.gradle
@@ -102,14 +102,13 @@ dependencies {
     testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21'
     testImplementation 'io.ktor:ktor-client-mock:2.2.4'
     testImplementation 'com.kohlschutter.junixsocket:junixsocket-core:2.6.2'
-    testImplementation project(":bank")
 }
 
 test {
     useJUnit()
     failFast = true
     testLogging.showStandardStreams = false
-    environment.put("LIBEUFIN_SANDBOX_ADMIN_PASSWORD", "foo")
+    environment.put("LIBEUFIN_BANK_ADMIN_PASSWORD", "foo")
     environment.put("LIBEUFIN_CASHOUT_TEST_TAN", "foo")
 }
 
@@ -132,4 +131,4 @@ run {
 task pofi(type: JavaExec) {
     classpath = sourceSets.test.runtimeClasspath
     mainClass = "PostFinanceKt"
-}
\ No newline at end of file
+}
diff --git a/nexus/src/test/kotlin/ConversionServiceTest.kt 
b/nexus/src/test/kotlin/ConversionServiceTest.kt
deleted file mode 100644
index f38dece0..00000000
--- a/nexus/src/test/kotlin/ConversionServiceTest.kt
+++ /dev/null
@@ -1,395 +0,0 @@
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.client.*
-import io.ktor.client.engine.cio.*
-import io.ktor.client.engine.mock.*
-import io.ktor.client.request.*
-import io.ktor.http.*
-import io.ktor.server.testing.*
-import kotlinx.coroutines.*
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Test
-import tech.libeufin.nexus.server.nexusApp
-import tech.libeufin.sandbox.*
-import tech.libeufin.util.parseAmount
-import java.math.BigDecimal
-
-class ConversionServiceTest {
-    // Tests the helper that fetches the new cash-out's to POST to Nexus.
-    @Test
-    fun testCashoutFetcher() {
-        withTestDatabase {
-            prepSandboxDb()
-            // making a transaction that means cash-out for bar, but not for 
foo.
-            // That lets test the singleton and empty result sets.
-            wireTransfer(
-                debitAccount = "foo",
-                creditAccount = "bar",
-                subject = "a cash-out for bar",
-                amount = "TESTKUDOS:3"
-            )
-            // Expecting the fetcher to return an empty set.
-            val expectEmpty = getUnsubmittedTransactions("foo")
-            assert(expectEmpty.isEmpty())
-            // Expecting the fetcher to return a one-element set.
-            val expectOne = getUnsubmittedTransactions("bar")
-            assert(expectOne.size == 1)
-            // Generating a bunch of cash-out operations for "foo"
-            for (i in 1..5)
-                wireTransfer(
-                    debitAccount = "bar",
-                    creditAccount = "foo",
-                    subject = "foo #$i",
-                    amount = "TESTKUDOS:3"
-                )
-            // Expecting 5 entries for foo.
-            val expectFive = getUnsubmittedTransactions("foo")
-            assert(expectFive.size == 5)
-            /* Checking the order.  The order should ensure that
-            * later payments get higher indexes.  */
-            assert(expectFive[0].subject == "foo #1")
-            assert(expectFive[4].subject == "foo #5")
-        }
-    }
-    // Tests the helper that applies buy-in ratio and fees
-    @Test
-    fun buyinRatioTest() {
-        val highFees = RatioAndFees(
-            buy_at_ratio = 1F,
-            buy_in_fee = 10F
-        )
-        // Checks that negatives aren't let through.
-        assertException<UtilError>({
-            applyBuyinRatioAndFees(
-            BigDecimal.ONE,
-            highFees)
-        })
-        // Checks successful case.
-        val fees = RatioAndFees(
-            buy_at_ratio = 3.5F,
-            buy_in_fee = 0.33F
-        )
-        assert(applyBuyinRatioAndFees(BigDecimal.valueOf(3), fees) == 
BigDecimal("10.17"))
-    }
-    private fun CoroutineScope.launchBuyinMonitor(httpClient: HttpClient): Job 
{
-        val job = launch {
-            /**
-             * The runInterruptible wrapper lets code without suspension
-             * points be cancel()'d.  Without it, such code would ignore
-             * any call to cancel() and the test never return.
-             */
-            runInterruptible {
-                buyinMonitor(
-                    demobankName = "default",
-                    accountToCredit = "exchange-0",
-                    client = httpClient
-                )
-            }
-        }
-        return job
-    }
-    /**
-     * Testing the buy-in monitor in all the HTTP scenarios,
-     * successful case, client's and server's error cases.
-     */
-    @Test
-    fun buyinTest() {
-        // 1, testing the successful case.
-        /* First create an incoming fiat payment _at Nexus_.
-          This payment is addressed to the Nexus user whose
-          (Nexus) credentials will be used by Sandbox to fetch
-          new incoming fiat payments. */
-        withTestDatabase {
-            prepSandboxDb(currency = "REGIO")
-            prepNexusDb()
-            // Credits 22 TESTKUDOS to "foo".  This information comes
-            // normally from the fiat bank that Nexus is connected to.
-            val reservePub = 
"GX5H5RME193FDRCM1HZKERXXQ2K21KH7788CKQM8X6MYKYRBP8F0"
-            newNexusBankTransaction(
-                currency = "TESTKUDOS",
-                value = "22",
-                /**
-                 * If the subject does NOT have the format of a public key,
-                 * the conversion service does NOT wire any regio amount to the
-                 * exchange, just ignores it.
-                 */
-                subject = reservePub
-            )
-            // Start Nexus, to let it serve the fiat transaction.
-            testApplication {
-                val client = this.createClient {
-                    followRedirects = false
-                }
-                application(nexusApp)
-                // Start the buy-in monitor to let it download the fiat 
transaction.
-                runBlocking {
-                    val job = launchBuyinMonitor(client)
-                    delay(1000L) // Lets the DB persist.
-                    job.cancelAndJoin()
-                }
-            }
-            // Checking that exchange got the converted amount.
-            transaction {
-                /**
-                 * Asserting that the exchange has only one incoming 
transaction.
-                 *
-                 * The Sandbox DB has two entries where the exchange IBAN shows
-                 * as the 'creditorIban': one DBIT related to the "admin" 
account,
-                 * and one CRDT related to the "exchange-0" account.  Thus 
filtering
-                 * the direction is also required.
-                 */
-                assert(
-                    BankAccountTransactionEntity.find {
-                        BankAccountTransactionsTable.creditorIban eq 
"AT561936082973364859" and (
-                            BankAccountTransactionsTable.direction eq "CRDT"
-                        )
-                    }.count() == 1L
-                )
-                val boughtIn = BankAccountTransactionEntity.find {
-                    BankAccountTransactionsTable.creditorIban eq 
"AT561936082973364859"
-                }.first()
-                // Asserting that the one incoming transaction has the wired 
reserve public key
-                // and the regional currency.
-                assert(boughtIn.subject == reservePub && boughtIn.currency == 
"REGIO")
-            }
-            // 2, testing the client side error case.
-            assertException<BuyinClientError>(
-                {
-                    runBlocking {
-                        /**
-                         * As soon as the buy-in monitor requests again the 
history
-                         * to Nexus, it'll get 400 from the mock client.
-                         */
-                        launchBuyinMonitor(getMockedClient { 
respondBadRequest() })
-                    }
-                }
-            )
-            /**
-             * 3, testing the server side error case.  Here the monitor should
-             * NOT throw any error and instead keep operating normally.  This 
allows
-             * Sandbox to tolerate server errors and retry the requests.
-             */
-            runBlocking {
-                /**
-                 * As soon as the buy-in monitor requests again the history
-                 * to Nexus, it'll get 500 from the mock client.
-                 */
-                val job = launchBuyinMonitor(getMockedClient { 
respondError(HttpStatusCode.InternalServerError) })
-                delay(1000L)
-                // Getting here means no exceptions.  Can now cancel the 
service.
-                job.cancelAndJoin()
-            }
-            /**
-             * 4, testing the unhandled error case.  This case is treated
-             * as a client error, to signal the calling logic to intervene.
-             */
-            assertException<BuyinClientError>(
-                {
-                    runBlocking {
-                        /**
-                         * As soon as the buy-in monitor requests again the 
history
-                         * to Nexus, it'll get 307 from the mock client.
-                         */
-                        launchBuyinMonitor(getMockedClient { respondRedirect() 
})
-                    }
-                }
-            )
-        }
-    }
-    private fun CoroutineScope.launchCashoutMonitor(httpClient: HttpClient): 
Job {
-        val job = launch {
-            /**
-             * The runInterruptible wrapper lets code without suspension
-             * points be cancel()'d.  Without it, such code would ignore
-             * any call to cancel() and the test never return.
-             */
-            runInterruptible {
-                /**
-                 * Without the runBlocking wrapper, cashoutMonitor doesn't
-                 * compile.  That's because it is a 'suspend' function and
-                 * it needs a coroutine environment to execute; 
runInterruptible
-                 * does NOT provide one.  Furthermore, replacing runBlocking
-                 * with "launch {}" would nullify runInterruptible, due to 
other
-                 * jobs that cashoutMonitor internally launches and would 
escape
-                 * the interruptible policy.
-                 */
-                runBlocking { cashoutMonitor(httpClient) }
-            }
-        }
-        return job
-    }
-
-    // This function mocks a 500 response to a cash-out request.
-    private fun MockRequestHandleScope.mock500Response(): HttpResponseData {
-        return respondError(HttpStatusCode.InternalServerError)
-    }
-    // This function implements a mock server that checks the currency in the 
cash-out request.
-    private suspend fun MockRequestHandleScope.inspectCashoutCurrency(request: 
HttpRequestData): HttpResponseData {
-        // Asserting that the currency is indeed the FIAT.
-        return if (request.url.encodedPath == 
"/bank-accounts/foo/payment-initiations" && request.method == HttpMethod.Post) {
-            val body = 
jacksonObjectMapper().readTree(request.body.toByteArray())
-            val postedAmount = body.get("amount").asText()
-            assert(parseAmount(postedAmount).currency == "FIAT")
-            respondOk("cash-out-nonce")
-        } else {
-            println("Cash-out monitor wrongly requested to: ${request.url}")
-            // This is a minimal Web server that support only the above 
endpoint.
-            respondError(status = HttpStatusCode.NotImplemented)
-        }
-    }
-
-    /**
-     * Checks that the cash-out monitor reacts after
-     * a CRDT transaction arrives at the designated account.
-     */
-    @Test
-    fun cashoutTest() {
-        withTestDatabase {
-            prepSandboxDb(
-                currency = "REGIO",
-                cashoutCurrency = "FIAT"
-            )
-            prepNexusDb()
-            testApplication {
-                val client = this.createClient {
-                    followRedirects = false
-                }
-                application(nexusApp)
-                // Mock server to intercept and inspect the cash-out request.
-                val checkCurrencyClient = HttpClient(MockEngine) {
-                    followRedirects = false
-                    engine {
-                        addHandler { 
-                            request -> inspectCashoutCurrency(request)
-                        }
-                    }
-                }
-                // Starting the cash-out monitor with the mocked client.
-                runBlocking {
-                    var job = launchCashoutMonitor(checkCurrencyClient)
-                    // Following are various cases of a cash-out scenario.
-
-                    /**
-                     * 1, Ordinary/successful case.  We test that the 
conversion
-                     * service sent indeed one request to Nexus and that the 
currency
-                     * is correct.
-                     */
-                    wireTransfer(
-                        debitAccount = "foo",
-                        creditAccount = "admin",
-                        subject = "fiat #0",
-                        amount = "REGIO:3"
-                    )
-                    delay(1000L) // Lets DB persist the information.
-                    // Checking now the Sandbox side, and namely that one
-                    // cash-out operation got carried out.
-                    transaction {
-                        assert(CashoutSubmissionEntity.all().count() == 1L)
-                        val op = CashoutSubmissionEntity.all().first()
-                        /**
-                         * The next assert witnesses that the mock client's
-                         * currency assert succeeded.
-                         */
-                        assert(op.maybeNexusResposnse == "cash-out-nonce")
-                    }
-                    /* 2, Internal server error case.  We test that after 
requesting
-                     * to a failing Nexus, the last accounted cash-out did NOT 
increase.
-                     */
-                    job.cancelAndJoin()
-                    val error500Client = HttpClient(MockEngine) {
-                        followRedirects = false
-                        engine {
-                            addHandler {
-                                    request -> mock500Response()
-                            }
-                        }
-                    }
-                    job = launchCashoutMonitor(error500Client)
-                    // Sending a new payment to trigger the conversion service.
-                    wireTransfer(
-                        debitAccount = "foo",
-                        creditAccount = "admin",
-                        subject = "fiat #1",
-                        amount = "REGIO:2"
-                    )
-                    delay(1000L) // Lets the reaction complete.
-                    job.cancelAndJoin()
-                    transaction {
-                        val bankaccount = getBankAccountFromLabel("admin")
-                        // Checks that the counter did NOT increase.
-                        assert(bankaccount.lastFiatSubmission?.id?.value == 1L)
-                    }
-                    /* Removing now the mocked 500 response and checking that
-                     * the problematic cash-out get then sent.  */
-                    job = launchCashoutMonitor(client) // Should find the non 
cashed-out wire transfer and react.
-                    delay(1000L) // Lets the reaction complete.
-                    job.cancelAndJoin()
-                    transaction {
-                        val bankaccount = getBankAccountFromLabel("admin")
-                        // Checks that the once failing cash-out did go 
through.
-                        assert(bankaccount.lastFiatSubmission?.subject == 
"fiat #1")
-                    }
-                    /**
-                     * 3, testing the client error case, where
-                     * the conversion service is supposed to throw exception.
-                     */
-                    assertException<CashoutClientError>({
-                        runBlocking {
-                            launchCashoutMonitor(
-                                httpClient = getMockedClient {
-                                    tech.libeufin.sandbox.logger.debug("MOCK 
400")
-                                    /**
-                                     * This causes the cash-out request sent 
to Nexus to
-                                     * respond with 400.
-                                     */
-                                    respondBadRequest()
-                                }
-                            )
-                            // Triggering now a cash-out operation via a new 
wire transfer to admin.
-                            wireTransfer(
-                                debitAccount = "foo",
-                                creditAccount = "admin",
-                                subject = "fiat #2",
-                                amount = "REGIO:22"
-                            )
-                        }})
-                    /**
-                     * 4, checking a redirect response.  Because this is an 
unhandled
-                     * error case, it is treated as a client error.  No need 
to wire a
-                     * new cash-out to trigger a cash-out request, since the 
last failed
-                     * one will be retried.
-                     */
-                    assertException<CashoutClientError>({
-                        runBlocking {
-                            launchCashoutMonitor(
-                                getMockedClient {
-                                    /**
-                                     * This causes the cash-out request sent 
to Nexus to
-                                     * respond with 307 Temporary Redirect.
-                                     */
-                                    respondRedirect()
-                                }
-                            )
-                        }
-                    })
-                    /* 5, Mocking a network error.  The previous failed 
cash-out
-                       will again trigger the service to POST to Nexus.  Here 
the
-                       monitor tolerates the failure, as it's not due to its 
state
-                       and should be temporary.
-                     */
-                    var requestMade = false
-                    job = launchCashoutMonitor(
-                        getMockedClient {
-                            requestMade = true
-                            throw Exception("Network Issue.")
-                        }
-                    )
-                    delay(2000L) // Lets the reaction complete.
-                    // asserting that the service is still running after the 
failed request.
-                    assert(requestMade && job.isActive)
-                    job.cancelAndJoin()
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/DbEventTest.kt 
b/nexus/src/test/kotlin/DbEventTest.kt
deleted file mode 100644
index d3922a76..00000000
--- a/nexus/src/test/kotlin/DbEventTest.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Test
-import tech.libeufin.util.NotificationsChannelDomains
-import tech.libeufin.util.PostgresListenHandle
-import tech.libeufin.util.buildChannelName
-import tech.libeufin.util.postgresNotify
-
-
-class DbEventTest {
-    /**
-     * LISTENs to one DB channel but only wakes up
-     * if the payload is how expected.
-     */
-    @Test
-    fun payloadTest() {
-        withTestDatabase {
-            val listenHandle = PostgresListenHandle("X")
-            transaction { listenHandle.postgresListen() }
-            runBlocking {
-                launch {
-                    val isArrived = listenHandle.waitOnIoDispatchersForPayload(
-                        timeoutMs = 1000L,
-                        expectedPayload = "Y"
-                    )
-                    assert(isArrived)
-                }
-                launch {
-                    delay(500L); // Ensures the wait helper runs first.
-                    transaction { this.postgresNotify("X", "Y") }
-                }
-            }
-        }
-    }
-
-    /**
-     * This function tests the NOTIFY sent by a Exposed's
-     * "new {}" overridden static method.
-     */
-    @Test
-    fun automaticNotifyTest() {
-        withTestDatabase {
-            prepNexusDb()
-            val nexusTxChannel = buildChannelName(
-                NotificationsChannelDomains.LIBEUFIN_NEXUS_TX,
-                "foo" // bank account label.
-            )
-            val listenHandle = PostgresListenHandle(nexusTxChannel)
-            transaction { listenHandle.postgresListen() }
-            runBlocking {
-                launch {
-                    val isArrived = listenHandle.waitOnIODispatchers(timeoutMs 
= 1000L)
-                    assert(isArrived)
-                }
-                launch {
-                    delay(500L); // Ensures the wait helper runs first.
-                    transaction {
-                        newNexusBankTransaction(
-                            "TESTKUDOS",
-                            "2",
-                            "unblocking event"
-                        )
-                    }
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/EbicsTest.kt 
b/nexus/src/test/kotlin/EbicsTest.kt
deleted file mode 100644
index e004070f..00000000
--- a/nexus/src/test/kotlin/EbicsTest.kt
+++ /dev/null
@@ -1,383 +0,0 @@
-import io.ktor.server.application.*
-import io.ktor.http.*
-import io.ktor.server.plugins.contentnegotiation.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import io.ktor.server.testing.*
-import kotlinx.coroutines.runBlocking
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Test
-import org.w3c.dom.Document
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.bankaccount.addPaymentInitiation
-import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
-import tech.libeufin.nexus.bankaccount.submitAllPaymentInitiations
-import tech.libeufin.nexus.ebics.*
-import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData
-import tech.libeufin.nexus.iso20022.createPain001document
-import tech.libeufin.nexus.server.*
-import tech.libeufin.sandbox.*
-import tech.libeufin.util.*
-import tech.libeufin.util.ebics_h004.EbicsRequest
-import tech.libeufin.util.ebics_h004.EbicsResponse
-import tech.libeufin.util.ebics_h004.EbicsTypes
-import tech.libeufin.util.ebics_h005.Ebics3Request
-import java.time.LocalDate
-import java.time.ZonedDateTime
-
-/**
- * These test cases run EBICS CCT and C52, mixing ordinary operations
- * and some error cases.
- */
-
-/**
- * Data to make the test server return for EBICS
- * phases.  Currently only init is supported.
- */
-data class EbicsResponses(
-    val init: String,
-    val download: String? = null,
-    val receipt: String? = null
-)
-
-/**
- * Minimal server responding always the 'init' field of a EbicsResponses
- * object to a download EBICS message.  Suitable to set arbitrary data
- * in said response.  Signs the response assuming the client is the one
- * created in MakeEnv.kt.
- */
-fun getCustomEbicsServer(r: EbicsResponses, endpoint: String = "/ebicsweb"): 
Application.() -> Unit {
-    val ret: Application.() -> Unit = {
-        install(ContentNegotiation) {
-            register(ContentType.Text.Xml, XMLEbicsConverter())
-            register(ContentType.Text.Plain, XMLEbicsConverter())
-        }
-        routing {
-            post(endpoint) {
-                val requestDocument = this.call.receive<Document>()
-                val req = requestDocument.toObject<EbicsRequest>()
-                val clientKey = 
CryptoUtil.loadRsaPublicKey(userKeys.enc.public.encoded)
-                val msgId = EbicsOrderUtil.generateTransactionId()
-                val resp: EbicsResponse = if (
-                    req.header.mutable.transactionPhase == 
EbicsTypes.TransactionPhaseType.INITIALISATION
-                ) {
-                    val payload = prepareEbicsPayload(r.init, clientKey)
-                    EbicsResponse.createForDownloadInitializationPhase(
-                        msgId,
-                        1,
-                        4096,
-                        payload.second, // for key material
-                        payload.first // actual payload
-                    )
-                } else {
-                    // msgId doesn't have to match the one used for the init 
phase.
-                    EbicsResponse.createForDownloadReceiptPhase(msgId, true)
-                }
-                val sigEbics = XMLUtil.signEbicsResponse(
-                    resp,
-                    CryptoUtil.loadRsaPrivateKey(bankKeys.auth.private.encoded)
-                )
-                call.respond(sigEbics)
-            }
-        }
-    }
-    return ret
-}
-
-class DownloadAndSubmit {
-    // Downloads a C52 report from the bank.
-    @Test
-    fun download() {
-        withNexusAndSandboxUser {
-            wireTransfer(
-                "admin",
-                "foo",
-                "default",
-                "Show up in logging!",
-                "TESTKUDOS:1"
-            )
-            wireTransfer(
-                "admin",
-                "foo",
-                "default",
-                "Exist in logging!",
-                "TESTKUDOS:5"
-            )
-
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    fetchBankAccountTransactions(
-                        client,
-                        fetchSpec = FetchSpecTimeRangeJson(
-                            level = FetchLevel.REPORT,
-                            start = "2020-10-10",
-                            end = "3000-10-10",
-                            bankConnection = "foo"
-                        ),
-                        accountId = "foo"
-                    )
-                }
-                transaction {
-                    // FIXME: assert on the subject.
-                    assert(
-                        NexusBankTransactionEntity[1].amount == "1" &&
-                                NexusBankTransactionEntity[2].amount == "5"
-                    )
-                }
-            }
-        }
-    }
-
-    // Uploads one payment instruction to the bank.
-    @Test
-    fun upload() {
-        withNexusAndSandboxUser {
-            testApplication {
-                application(sandboxApp)
-                val conn = EbicsBankConnectionProtocol()
-                runBlocking {
-                    // Create Pain.001 to be submitted.
-                    addPaymentInitiation(
-                        Pain001Data(
-                            creditorIban = BAR_USER_IBAN,
-                            creditorBic = "SANDBOXX",
-                            creditorName = "Tester",
-                            subject = "test payment",
-                            sum = "1",
-                            currency = "TESTKUDOS"
-                        ),
-                        transaction {
-                            NexusBankAccountEntity.findByName(
-                                "foo"
-                            ) ?: throw Exception("Test failed")
-                        }
-                    )
-                    conn.submitPaymentInitiation(
-                        client,
-                        1L
-                    )
-                }
-                transaction {
-                    val howMany = BankAccountTransactionEntity.find {
-                        BankAccountTransactionsTable.debtorIban eq 
FOO_USER_IBAN and (
-                            BankAccountTransactionsTable.subject eq "test 
payment"
-                        ) and (BankAccountTransactionsTable.direction eq 
"DBIT")
-                    }.count()
-                    assert(howMany == 1L)
-                }
-            }
-        }
-    }
-
-    /**
-     * Upload one payment instruction charging one IBAN
-     * that does not belong to the requesting EBICS subscriber.
-     */
-    @Test
-    fun unallowedDebtorIban() {
-        withNexusAndSandboxUser {
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    val bar = transaction { 
NexusBankAccountEntity.findByName("bar") }
-                    val painMessage = createPain001document(
-                        NexusPaymentInitiationData(
-                            debtorIban = bar!!.iban,
-                            debtorBic = bar.bankCode,
-                            debtorName = bar.accountHolder,
-                            currency = "TESTKUDOS",
-                            amount = "1",
-                            creditorIban = getIban(),
-                            creditorName = "Get",
-                            creditorBic = "SANDBOXX",
-                            paymentInformationId = "entropy-0",
-                            preparationTimestamp = 1970L,
-                            subject = "Unallowed",
-                            messageId = "entropy-1",
-                            endToEndId = null,
-                            instructionId = null
-                        )
-                    )
-                    val unallowedSubscriber = transaction { 
getEbicsSubscriberDetails("foo") }
-                    var thrown = false
-                    try {
-                        doEbicsUploadTransaction(
-                            client,
-                            unallowedSubscriber,
-                            EbicsUploadSpec(
-                                orderType = "CCT",
-                                isEbics3 = false,
-                                orderParams = EbicsStandardOrderParams()
-                            ),
-                            painMessage.toByteArray(Charsets.UTF_8)
-                        )
-                    } catch (e: EbicsProtocolError) {
-                        if (e.ebicsTechnicalCode ==
-                                
EbicsReturnCode.EBICS_ACCOUNT_AUTHORISATION_FAILED
-                        )
-                            thrown = true
-                    }
-                    assert(thrown)
-                }
-            }
-        }
-    }
-
-    /**
-     * Submits one pain.001 document with the wrong currency and checks
-     * that the bank responded with EBICS_PROCESSING_ERROR.
-     */
-    @Test
-    fun unsupportedCurrency() {
-        withNexusAndSandboxUser {
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    // Create Pain.001 to be submitted.
-                    addPaymentInitiation(
-                        Pain001Data(
-                            creditorIban = getIban(),
-                            creditorBic = "SANDBOXX",
-                            creditorName = "Tester",
-                            subject = "test payment",
-                            sum = "1",
-                            currency = "EUR" // EUR not supported.
-                        ),
-                        transaction {
-                            NexusBankAccountEntity.findByName("foo") ?: throw 
Exception("Test failed")
-                        }
-                    )
-                    var thrown = false
-                    try {
-                        submitAllPaymentInitiations(client, "foo")
-                    } catch (e: EbicsProtocolError) {
-                        if (e.ebicsTechnicalCode == 
EbicsReturnCode.EBICS_PROCESSING_ERROR)
-                            thrown = true
-                    }
-                    assert(thrown)
-                }
-            }
-        }
-    }
-
-    /**
-     * Test that pain.001 amounts ALSO have max 2 fractional digits, like 
Taler's.
-     * That makes Sandbox however NOT completely compatible with the pain.001 
standard,
-     * since this allows up to 5 fractional digits.  */
-    @Test
-    fun testFractionalDigits() {
-        withNexusAndSandboxUser {
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    // Create Pain.001 with excessive amount.
-                    addPaymentInitiation(
-                        Pain001Data(
-                            creditorIban = getIban(),
-                            creditorBic = "SANDBOXX",
-                            creditorName = "Tester",
-                            subject = "test payment",
-                            sum = "1.001", // wrong 3 fractional digits.
-                            currency = "TESTKUDOS"
-                        ),
-                        "foo"
-                    )
-                    assertException<EbicsProtocolError>({ 
submitAllPaymentInitiations(client, "foo") })
-                }
-            }
-        }
-    }
-
-    // Test the EBICS error message in case of debt threshold being surpassed
-    @Test
-    fun testDebit() {
-        withNexusAndSandboxUser {
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    // Create Pain.001 with excessive amount.
-                    addPaymentInitiation(
-                        Pain001Data(
-                            creditorIban = getIban(),
-                            creditorBic = "SANDBOXX",
-                            creditorName = "Tester",
-                            subject = "test payment",
-                            sum = "1000000",
-                            currency = "TESTKUDOS"
-                        ),
-                        "foo"
-                    )
-                    assertException<EbicsProtocolError>(
-                        { submitAllPaymentInitiations(client, "foo") },
-                        {
-                            val nexusEbicsException = it as EbicsProtocolError
-                            assert(
-                                
EbicsReturnCode.EBICS_AMOUNT_CHECK_FAILED.errorCode ==
-                                
nexusEbicsException.ebicsTechnicalCode?.errorCode
-                            )
-                        }
-                    )
-                }
-            }
-        }
-    }
-}
-
-class EbicsTest {
-
-    @Test
-    fun genEbics3Upload() {
-        withTestDatabase {
-            prepNexusDb()
-            val foo = transaction { getEbicsSubscriberDetails("foo") }
-            val uploadDoc = createEbicsRequestForUploadInitialization(
-                subscriberDetails = foo,
-                ebics3OrderService = 
Ebics3Request.OrderDetails.Service().apply {
-                    serviceName = "OTH"
-                    scope = "BIL"
-                    serviceOption = "CH002LMF"
-                    messageName = 
Ebics3Request.OrderDetails.Service.MessageName().apply {
-                        value = "csv"
-                    }
-                },
-                null,
-                prepareUploadPayload(
-                    foo,
-                    "foo".toByteArray(),
-                    isEbics3 = true
-                )
-            )
-            assert(XMLUtil.validateFromString(uploadDoc))
-        }
-    }
-
-    /**
-     * Tests the validity of EBICS 3.0 messages.
-     */
-    @Test
-    fun genEbics3Download() {
-        withTestDatabase {
-            prepNexusDb()
-            val foo = transaction { getEbicsSubscriberDetails("foo") }
-            val downloadDoc = createEbicsRequestForDownloadInitialization(
-                subscriberDetails = foo,
-                ebics3OrderService = 
Ebics3Request.OrderDetails.Service().apply {
-                    messageName = 
Ebics3Request.OrderDetails.Service.MessageName().apply {
-                        value = "camt.054"
-                        version = "04"
-                    }
-                    scope = "CH"
-                    serviceName = "REP"
-                    container = 
Ebics3Request.OrderDetails.Service.Container().apply {
-                        containerType = "ZIP"
-                    }
-                },
-                orderParams = EbicsStandardOrderParams()
-            )
-            assert(XMLUtil.validateFromString(downloadDoc))
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/Iso20022Test.kt 
b/nexus/src/test/kotlin/Iso20022Test.kt
deleted file mode 100644
index 06d14ce1..00000000
--- a/nexus/src/test/kotlin/Iso20022Test.kt
+++ /dev/null
@@ -1,204 +0,0 @@
-package tech.libeufin.nexus
-import CamtBankAccountEntry
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.http.*
-import io.ktor.server.testing.*
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Ignore
-import org.junit.Test
-import org.w3c.dom.Document
-import poFiCamt054_2019_incoming
-import poFiCamt054_2019_outgoing
-import prepNexusDb
-import tech.libeufin.nexus.iso20022.*
-import tech.libeufin.nexus.server.EbicsDialects
-import tech.libeufin.nexus.server.FetchLevel
-import tech.libeufin.util.DestructionError
-import tech.libeufin.util.XMLUtil
-import tech.libeufin.util.destructXml
-import tech.libeufin.util.getNow
-import withTestDatabase
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
-import kotlin.test.assertTrue
-
-fun loadXmlResource(name: String): Document {
-    val classLoader = ClassLoader.getSystemClassLoader()
-    val res = classLoader.getResource(name)
-    if (res == null) {
-        throw Exception("resource $name not found");
-    }
-    return XMLUtil.parseStringIntoDom(res.readText())
-}
-
-class Iso20022Test {
-    @Test(expected = DestructionError::class)
-    fun testUniqueChild() {
-        val xml = """
-            <a>
-              <b/>
-              <b/>
-            </a>
-        """.trimIndent()
-        // when XML is invalid, DestructionError is thrown.
-        val doc = XMLUtil.parseStringIntoDom(xml)
-        destructXml(doc) {
-            requireRootElement("a") {
-                requireOnlyChild {  }
-            }
-        }
-    }
-
-    /**
-     * This test is currently ignored because the Camt sample being parsed
-     * contains a money movement which is not a singleton.  This is not in
-     * line with the current parsing logic (that expects the style used by GLS)
-     */
-    @Ignore
-    fun testTransactionsImport() {
-        val camt53 = 
loadXmlResource("iso20022-samples/camt.053/de.camt.053.001.02.xml")
-        val r = parseCamtMessage(camt53)
-        assertEquals("msg-001", r.messageId)
-        assertEquals("2020-07-03T12:44:40+05:30", r.creationDateTime)
-        assertEquals(CashManagementResponseType.Statement, r.messageType)
-        assertEquals(1, r.reports.size)
-
-        // First Entry
-        assertTrue("100" == r.reports[0].entries[0].amount.value)
-        assertEquals("EUR", r.reports[0].entries[0].amount.currency)
-        assertEquals(CreditDebitIndicator.CRDT, 
r.reports[0].entries[0].creditDebitIndicator)
-        assertEquals(EntryStatus.BOOK, r.reports[0].entries[0].status)
-        assertEquals(null, r.reports[0].entries[0].entryRef)
-        assertEquals("acctsvcrref-001", 
r.reports[0].entries[0].accountServicerRef)
-        assertEquals("PMNT-RCDT-ESCT", 
r.reports[0].entries[0].bankTransactionCode)
-        assertNotNull(r.reports[0].entries[0].batches?.get(0))
-        assertEquals(
-            "unstructured info one",
-            
r.reports[0].entries[0].batches?.get(0)?.batchTransactions?.get(0)?.details?.unstructuredRemittanceInformation
-        )
-
-        // Second Entry
-        assertEquals(
-            "unstructured info across lines",
-            
r.reports[0].entries[1].batches?.get(0)?.batchTransactions?.get(0)?.details?.unstructuredRemittanceInformation
-        )
-
-        // Third Entry
-        // Make sure that round-tripping of entry CamtBankAccountEntry JSON 
works
-        for (entry in r.reports.flatMap { it.entries }) {
-            val txStr = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(entry)
-            val tx2 = jacksonObjectMapper().readValue(txStr, 
CamtBankAccountEntry::class.java)
-            val tx2Str = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(tx2)
-            assertEquals(jacksonObjectMapper().readTree(txStr), 
jacksonObjectMapper().readTree(tx2Str))
-        }
-
-        
println(jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(r))
-    }
-
-    /**
-     * PoFi timestamps aren't zoned, therefore the usual ZonedDateTime
-     * doesn't cover it.  They must switch to (java.time.)LocalDateTime.
-     */
-    @Test
-    fun parsePostFinanceDate() {
-        // 2011-12-03T10:15:30 from Java Doc as ISO_LOCAL_DATE_TIME.
-        // 2023-05-09T11:04:09 from PoFi
-
-        getTimestampInMillis(
-            "2011-12-03T10:15:30",
-            EbicsDialects.POSTFINANCE.dialectName
-        )
-        getTimestampInMillis(
-            "2011-12-03T10:15:30Z" // ! with timezone
-        )
-    }
-
-    @Test
-    fun parsePoFiCamt054() {
-        val doc = XMLUtil.parseStringIntoDom(poFiCamt054_2019_incoming)
-        parseCamtMessage(doc, dialect = "pf")
-    }
-
-    /**
-     * Testing how outgoing payments get ingested and how their
-     * deduplication logic reacts, given that sometimes camt.054
-     * was seen without the AcctSvcrRef.
-     */
-    @Test
-    fun ingestPoFiCamt054_outgoing() {
-        val doc = XMLUtil.parseStringIntoDom(poFiCamt054_2019_outgoing)
-        withTestDatabase {
-            prepNexusDb()
-            transaction { assert(NexusBankTransactionEntity.all().count() == 
0L) }
-            ingestCamtMessageIntoAccount(
-                "foo",
-                doc,
-                FetchLevel.NOTIFICATION,
-                dialect = "pf"
-            )
-            transaction { assert(NexusBankTransactionEntity.all().count() == 
1L) }
-            // Checking that the payment doesn't get stored twice.
-            ingestCamtMessageIntoAccount(
-                "foo",
-                doc,
-                FetchLevel.NOTIFICATION,
-                dialect = "pf"
-            )
-            transaction { assert(NexusBankTransactionEntity.all().count() == 
1L) }
-        }
-    }
-
-    @Test
-    fun ingestPoFiCamt054() {
-        val doc = XMLUtil.parseStringIntoDom(poFiCamt054_2019_incoming)
-        withTestDatabase {
-            prepNexusDb()
-            // Checking that no transactions exist already in the database.
-            transaction { assert(NexusBankTransactionEntity.all().count() == 
0L) }
-            ingestCamtMessageIntoAccount(
-                "foo",
-                doc,
-                FetchLevel.NOTIFICATION,
-                dialect = "pf"
-            )
-            // Checking that now ONE transaction exist in the database.
-            transaction { assert(NexusBankTransactionEntity.all().count() == 
1L) }
-            // Checking now that the same payment doesn't get ingested twice.
-            ingestCamtMessageIntoAccount(
-                "foo",
-                doc,
-                FetchLevel.NOTIFICATION,
-                dialect = "pf"
-            )
-            // The count should have stayed the same.
-            transaction { assert(NexusBankTransactionEntity.all().count() == 
1L) }
-        }
-    }
-    // Checks that the 2019 pain.001 version validates.
-    @Test
-    fun validatePain001() {
-        val pain001 = createPain001document(
-            NexusPaymentInitiationData(
-                debtorIban = "CH0889144371988976754",
-                debtorBic = "POFICHBEXXX",
-                debtorName = "Sample Debtor Name",
-                currency = "CHF",
-                amount = "5.00",
-                creditorIban = "CH9789144829733648596",
-                creditorName = "Sample Creditor Name",
-                creditorBic = "POFICHBEXXX",
-                paymentInformationId = "8aae7a2ded2f",
-                preparationTimestamp = getNow().toInstant().toEpochMilli(),
-                subject = "Unstructured remittance information",
-                instructionId = "InstructionId",
-                endToEndId = "71cfbdaf901f",
-                messageId = "2a16b35ed69c"
-            ),
-            dialect = "pf"
-        )
-        val doc = XMLUtil.parseStringIntoDom(pain001)
-        assert(XMLUtil.validateFromDom(doc))
-    }
-}
diff --git a/nexus/src/test/kotlin/JsonTest.kt 
b/nexus/src/test/kotlin/JsonTest.kt
deleted file mode 100644
index 30e919d9..00000000
--- a/nexus/src/test/kotlin/JsonTest.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-import org.junit.Test
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import com.fasterxml.jackson.module.kotlin.readValue
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.ktor.server.testing.*
-import io.ktor.utils.io.jvm.javaio.*
-import org.junit.Ignore
-import tech.libeufin.nexus.server.CreateBankConnectionFromBackupRequestJson
-import tech.libeufin.nexus.server.CreateBankConnectionFromNewRequestJson
-import tech.libeufin.sandbox.NexusTransactions
-import tech.libeufin.sandbox.sandboxApp
-
-enum class EnumTest { TEST }
-data class EnumWrapper(val enum_test: EnumTest)
-
-class JsonTest {
-
-    @Test
-    fun testJackson() {
-        val mapper = jacksonObjectMapper()
-        val backupObj = CreateBankConnectionFromBackupRequestJson(
-            name = "backup", passphrase = "secret", data = 
mapper.readTree("{}")
-        )
-        val roundTrip = 
mapper.readValue<CreateBankConnectionFromBackupRequestJson>(mapper.writeValueAsString(backupObj))
-        assert(roundTrip.data.toString() == "{}" && roundTrip.passphrase == 
"secret" && roundTrip.name == "backup")
-        val newConnectionObj = CreateBankConnectionFromNewRequestJson(
-            name = "new-connection", type = "ebics", data = 
mapper.readTree("{}")
-        )
-        val roundTripNew = 
mapper.readValue<CreateBankConnectionFromNewRequestJson>(mapper.writeValueAsString(newConnectionObj))
-        assert(roundTripNew.data.toString() == "{}" && roundTripNew.type == 
"ebics" && roundTripNew.name == "new-connection")
-    }
-
-    // Tests how Jackson+Kotlin handle enum types.  Fails if an exception is 
thrown
-    @Test
-    fun enumTest() {
-        val m = jacksonObjectMapper()
-         m.readValue<EnumWrapper>("{\"enum_test\":\"TEST\"}")
-         m.readValue<EnumTest>("\"TEST\"")
-    }
-
-    /**
-     * Ignored because this test was only used to check
-     * the logs, as opposed to assert over values.  Consider
-     * to remove the Ignore
-     */
-    @Ignore
-    @Test
-    fun testSandboxJsonParsing() {
-        testApplication {
-            application(sandboxApp)
-            client.post("/admin/ebics/subscribers") {
-                basicAuth("admin", "foo")
-                contentType(ContentType.Application.Json)
-                setBody("{}")
-            }
-        }
-    }
-
-    data class CamtEntryWrapper(
-        val unusedValue: String,
-        val camtData: CamtBankAccountEntry
-    )
-
-    // Testing whether generating and parsing a CaMt JSON mapping works.
-    @Test
-    fun testCamtRoundTrip() {
-        val obj = genNexusIncomingCamt(
-            CurrencyAmount(value = "2", currency = "EUR"),
-            subject = "round trip test"
-        )
-        val str = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(obj)
-        val map = jacksonObjectMapper().readValue(str, 
CamtBankAccountEntry::class.java)
-        assert(str == 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(map))
-    }
-
-    @Test
-    fun parseRawJson() {
-        val camtModel = """
-            {
-              "amount" : "TESTKUDOS:22",
-              "creditDebitIndicator" : "CRDT",
-              "status" : "BOOK",
-              "bankTransactionCode" : "mock",
-              "batches" : [ {
-                "batchTransactions" : [ {
-                  "amount" : "TESTKUDOS:22",
-                  "creditDebitIndicator" : "CRDT",
-                  "details" : {
-                    "debtor" : {
-                      "name" : "Mock Payer"
-                    },
-                    "debtorAccount" : {
-                      "iban" : "MOCK-IBAN"
-                    },
-                    "debtorAgent" : {
-                      "bic" : "MOCK-BIC"
-                    },
-                    "unstructuredRemittanceInformation" : "raw"
-                  }
-                } ]
-              } ]
-            }
-        """.trimIndent()
-        val obj = jacksonObjectMapper().readValue(camtModel, 
CamtBankAccountEntry::class.java)
-        assert(obj.getSingletonSubject() == "raw")
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/LetterFormatTest.kt 
b/nexus/src/test/kotlin/LetterFormatTest.kt
deleted file mode 100644
index a268492d..00000000
--- a/nexus/src/test/kotlin/LetterFormatTest.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package tech.libeufin.nexus
-
-import org.junit.Test
-import tech.libeufin.util.chunkString
-import tech.libeufin.util.toHexString
-import java.security.SecureRandom
-
-/**
- * @param size in bits
- */
-private fun getNonce(size: Int): ByteArray {
-    val sr = SecureRandom()
-    val ret = ByteArray(size / 8)
-    sr.nextBytes(ret)
-    return ret
-}
-
-class LetterFormatTest {
-
-    @Test
-    fun chunkerTest() {
-        val blob = getNonce(1024)
-        println(chunkString(blob.toHexString()))
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt
deleted file mode 100644
index 32c6e607..00000000
--- a/nexus/src/test/kotlin/MakeEnv.kt
+++ /dev/null
@@ -1,772 +0,0 @@
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.client.*
-import io.ktor.client.engine.mock.*
-import io.ktor.client.request.*
-import org.jetbrains.exposed.sql.Database
-import org.jetbrains.exposed.sql.statements.api.ExposedBlob
-import org.jetbrains.exposed.sql.transactions.TransactionManager
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.dbCreateTables
-import tech.libeufin.nexus.dbDropTables
-import tech.libeufin.nexus.server.BankConnectionType
-import tech.libeufin.nexus.server.FetchLevel
-import tech.libeufin.nexus.server.FetchSpecAllJson
-import tech.libeufin.sandbox.*
-import tech.libeufin.util.*
-
-data class EbicsKeys(
-    val auth: CryptoUtil.RsaCrtKeyPair,
-    val enc: CryptoUtil.RsaCrtKeyPair,
-    val sig: CryptoUtil.RsaCrtKeyPair
-)
-// Convenience DB connection to switch to Postgresql:
-val currentUser = System.getProperty("user.name")
-
-val BANK_IBAN = getIban()
-val FOO_USER_IBAN = getIban()
-val BAR_USER_IBAN = getIban()
-val 
TCP_POSTGRES_CONN="jdbc:postgresql://localhost:5432/libeufincheck?user=$currentUser"
-val UNIX_SOCKET_CONN= "postgresql:///libeufincheck"
-val TEST_DB_CONN = UNIX_SOCKET_CONN
-
-val bankKeys = EbicsKeys(
-    auth = CryptoUtil.generateRsaKeyPair(2048),
-    enc = CryptoUtil.generateRsaKeyPair(2048),
-    sig = CryptoUtil.generateRsaKeyPair(2048)
-)
-val userKeys = EbicsKeys(
-    auth = CryptoUtil.generateRsaKeyPair(2048),
-    enc = CryptoUtil.generateRsaKeyPair(2048),
-    sig = CryptoUtil.generateRsaKeyPair(2048)
-)
-
-fun assertWithPrint(cond: Boolean, msg: String) {
-    try {
-        assert(cond)
-    } catch (e: AssertionError) {
-        System.err.println(msg)
-        throw e
-    }
-}
-
-// New versions of JUnit provide this!
-inline fun <reified ExceptionType> assertException(
-    block: () -> Unit,
-    assertBlock: (Throwable) -> Unit = {}
-) {
-    try {
-        block()
-    } catch (e: Throwable) {
-        assert(e.javaClass == ExceptionType::class.java)
-        // Expected type, try more custom asserts on it
-        assertBlock(e)
-        return
-    }
-    return assert(false)
-}
-
-/**
- * Run a block after connecting to the test database.
- * Cleans up the DB file afterwards.
- */
-fun withTestDatabase(keepData: Boolean = false, f: () -> Unit) {
-    if (!keepData) {
-        dbDropTables(TEST_DB_CONN)
-        tech.libeufin.sandbox.dbDropTables(TEST_DB_CONN)
-    }
-    f()
-}
-
-val reportSpec: String = jacksonObjectMapper().
-writerWithDefaultPrettyPrinter().
-writeValueAsString(
-    FetchSpecAllJson(
-        level = FetchLevel.REPORT,
-        "foo"
-    )
-)
-
-fun prepNexusDb() {
-    dbCreateTables(TEST_DB_CONN)
-    transaction {
-        val u = NexusUserEntity.new {
-            username = "foo"
-            passwordHash = CryptoUtil.hashpw("foo")
-            superuser = true
-        }
-        val b = NexusUserEntity.new {
-            username = "bar"
-            passwordHash = CryptoUtil.hashpw("bar")
-            superuser = true
-        }
-        val c = NexusBankConnectionEntity.new {
-            connectionId = "bar"
-            owner = b
-            type = "x-libeufin-bank"
-        }
-        val d = NexusBankConnectionEntity.new {
-            connectionId = "foo"
-            owner = b
-            type = "ebics"
-        }
-        XLibeufinBankUserEntity.new {
-            username = "bar"
-            password = "bar"
-            // Only addressing mild cases where ONE slash ends the base URL.
-            baseUrl = "http://localhost/demobanks/default/access-api";
-            nexusBankConnection = c
-        }
-        tech.libeufin.nexus.EbicsSubscriberEntity.new {
-            // ebicsURL = "http://localhost:5000/ebicsweb";
-            ebicsURL = "http://localhost/ebicsweb";
-            hostID = "eufinSandbox"
-            partnerID = "foo"
-            userID = "foo"
-            systemID = "foo"
-            signaturePrivateKey = ExposedBlob(userKeys.sig.private.encoded)
-            encryptionPrivateKey = ExposedBlob(userKeys.enc.private.encoded)
-            authenticationPrivateKey = 
ExposedBlob(userKeys.auth.private.encoded)
-            nexusBankConnection = d
-            ebicsIniState = EbicsInitState.NOT_SENT
-            ebicsHiaState = EbicsInitState.NOT_SENT
-            bankEncryptionPublicKey = ExposedBlob(bankKeys.enc.public.encoded)
-            bankAuthenticationPublicKey = 
ExposedBlob(bankKeys.auth.public.encoded)
-        }
-        NexusBankAccountEntity.new {
-            bankAccountName = "foo"
-            iban = FOO_USER_IBAN
-            bankCode = "SANDBOXX"
-            defaultBankConnection = d
-            highestSeenBankMessageSerialId = 0
-            accountHolder = "foo"
-        }
-        NexusBankAccountEntity.new {
-            bankAccountName = "bar"
-            iban = BAR_USER_IBAN
-            bankCode = "SANDBOXX"
-            defaultBankConnection = c
-            highestSeenBankMessageSerialId = 0
-            accountHolder = "bar"
-        }
-        NexusScheduledTaskEntity.new {
-            resourceType = "bank-account"
-            resourceId = "foo"
-            this.taskCronspec = "* * *" // Every second.
-            this.taskName = "read-report"
-            this.taskType = "fetch"
-            this.taskParams = reportSpec
-        }
-        NexusScheduledTaskEntity.new {
-            resourceType = "bank-account"
-            resourceId = "foo"
-            this.taskCronspec = "* * *" // Every second.
-            this.taskName = "send-payment"
-            this.taskType = "submit"
-            this.taskParams = "{}"
-        }
-        // Giving 'foo' a Taler facade.
-        val f = FacadeEntity.new {
-            facadeName = "foo-facade"
-            type = "taler-wire-gateway"
-            creator = u
-        }
-        FacadeStateEntity.new {
-            bankAccount = "foo"
-            bankConnection = "foo"
-            currency = "TESTKUDOS"
-            reserveTransferLevel = "report"
-            facade = f
-            highestSeenMessageSerialId = 0
-        }
-        // Giving 'bar' a Taler facade
-        val g = FacadeEntity.new {
-            facadeName = "bar-facade"
-            type = "taler-wire-gateway"
-            creator = b
-        }
-        FacadeStateEntity.new {
-            bankAccount = "bar"
-            bankConnection = "bar" // uses x-libeufin-bank connection.
-            currency = "TESTKUDOS"
-            reserveTransferLevel = "report"
-            facade = g
-            highestSeenMessageSerialId = 0
-        }
-    }
-}
-
-fun prepSandboxDb(
-    usersDebtLimit: Int = 1000,
-    currency: String = "TESTKUDOS",
-    cashoutCurrency: String = "EUR"
-) {
-    tech.libeufin.sandbox.dbCreateTables(TEST_DB_CONN)
-    transaction {
-        val config = DemobankConfig(
-            currency = currency,
-            cashoutCurrency = cashoutCurrency,
-            bankDebtLimit = 10000,
-            usersDebtLimit = usersDebtLimit,
-            allowRegistrations = true,
-            demobankName = "default",
-            withSignupBonus = false,
-            captchaUrl = "http://example.com/";,
-            suggestedExchangePayto = "payto://iban/${BAR_USER_IBAN}",
-            nexusBaseUrl = "http://localhost/";,
-            usernameAtNexus = "foo",
-            passwordAtNexus = "foo",
-            enableConversionService = true
-        )
-        insertConfigPairs(config)
-        val demoBank = DemobankConfigEntity.new { name = "default" }
-        BankAccountEntity.new {
-            iban = BANK_IBAN
-            label = "admin" // used by the wire helper
-            owner = "admin" // used by the person name finder
-            // For now, the model assumes always one demobank
-            this.demoBank = demoBank
-        }
-        EbicsHostEntity.new {
-            this.ebicsVersion = "3.0"
-            this.hostId = "eufinSandbox"
-            this.authenticationPrivateKey = 
ExposedBlob(bankKeys.auth.private.encoded)
-            this.encryptionPrivateKey = 
ExposedBlob(bankKeys.enc.private.encoded)
-            this.signaturePrivateKey = 
ExposedBlob(bankKeys.sig.private.encoded)
-        }
-        val bankAccount = BankAccountEntity.new {
-            iban = FOO_USER_IBAN
-            /**
-             * For now, keep same semantics of Pybank: a username
-             * is AS WELL a bank account label.  In other words, it
-             * identifies a customer AND a bank account.
-             */
-            label = "foo"
-            owner = "foo"
-            this.demoBank = demoBank
-            isPublic = false
-        }
-        BankAccountEntity.new {
-            iban = BAR_USER_IBAN
-            /**
-             * For now, keep same semantics of Pybank: a username
-             * is AS WELL a bank account label.  In other words, it
-             * identifies a customer AND a bank account.
-             */
-            label = "bar"
-            owner = "bar"
-            this.demoBank = demoBank
-            isPublic = false
-        }
-        tech.libeufin.sandbox.EbicsSubscriberEntity.new {
-            hostId = "eufinSandbox"
-            partnerId = "foo"
-            userId = "foo"
-            systemId = "foo"
-            signatureKey = EbicsSubscriberPublicKeyEntity.new {
-                rsaPublicKey = ExposedBlob(userKeys.sig.public.encoded)
-                state = KeyState.RELEASED
-            }
-            encryptionKey = EbicsSubscriberPublicKeyEntity.new {
-                rsaPublicKey = ExposedBlob(userKeys.enc.public.encoded)
-                state = KeyState.RELEASED
-            }
-            authenticationKey = EbicsSubscriberPublicKeyEntity.new {
-                rsaPublicKey = ExposedBlob(userKeys.auth.public.encoded)
-                state = KeyState.RELEASED
-            }
-            state = SubscriberState.INITIALIZED
-            nextOrderID = 1
-            this.bankAccount = bankAccount
-        }
-        DemobankCustomerEntity.new {
-            username = "foo"
-            passwordHash = CryptoUtil.hashpw("foo")
-            name = "Foo"
-            cashout_address = "payto://iban/OUTSIDE"
-        }
-        DemobankCustomerEntity.new {
-            username = "bar"
-            passwordHash = CryptoUtil.hashpw("bar")
-            name = "Bar"
-            cashout_address = "payto://iban/FIAT"
-        }
-        // Note: exchange doesn't have the cash-out address.
-        DemobankCustomerEntity.new {
-            username = "exchange-0"
-            passwordHash = CryptoUtil.hashpw("foo")
-            name = "Exchange"
-        }
-        BankAccountEntity.new {
-            iban = "AT561936082973364859"
-            /**
-             * For now, keep same semantics of Pybank: a username
-             * is AS WELL a bank account label.  In other words, it
-             * identifies a customer AND a bank account.
-             */
-            label = "exchange-0"
-            owner = "exchange-0"
-            this.demoBank = demoBank
-            isPublic = false
-        }
-    }
-}
-
-fun withNexusAndSandboxUser(f: () -> Unit) {
-    withTestDatabase {
-        prepNexusDb()
-        prepSandboxDb()
-        f()
-    }
-}
-
-// Creates tables, the default demobank, and admin's bank account.
-fun withSandboxTestDatabase(f: () -> Unit) {
-    withTestDatabase {
-        tech.libeufin.sandbox.dbCreateTables(TEST_DB_CONN)
-        transaction {
-            val config = DemobankConfig(
-                currency = "TESTKUDOS",
-                cashoutCurrency = "NOTUSED",
-                bankDebtLimit = 10000,
-                usersDebtLimit = 1000,
-                allowRegistrations = true,
-                demobankName = "default",
-                withSignupBonus = false,
-                captchaUrl = "http://example.com/"; // unused
-            )
-            insertConfigPairs(config)
-            val d = DemobankConfigEntity.new { name = "default" }
-            // admin's bank account.
-            BankAccountEntity.new {
-                iban = BANK_IBAN
-                label = "admin" // used by the wire helper
-                owner = "admin" // used by the person name finder
-                // For now, the model assumes always one demobank
-                this.demoBank = d
-            }
-        }
-        f()
-    }
-}
-
-fun newNexusBankTransaction(
-    currency: String,
-    value: String,
-    subject: String,
-    creditorAcct: String = "foo",
-    connType: BankConnectionType = BankConnectionType.EBICS
-) {
-    val jDetails: String = when(connType) {
-        BankConnectionType.EBICS -> {
-            jacksonObjectMapper(
-            ).writerWithDefaultPrettyPrinter(
-            ).writeValueAsString(
-                genNexusIncomingCamt(
-                    amount = CurrencyAmount(currency,value),
-                    subject = subject
-                )
-            )
-        }
-        /**
-         * Note: x-libeufin-bank ALSO stores the transactions in the
-         * CaMt representation, hence this branch should be removed.
-         */
-        BankConnectionType.X_LIBEUFIN_BANK -> {
-            jacksonObjectMapper(
-            ).writerWithDefaultPrettyPrinter(
-            ).writeValueAsString(genNexusIncomingCamt(
-                amount = CurrencyAmount(currency, value),
-                subject = subject
-            ))
-        }
-        else -> throw Exception("Unsupported connection type: 
${connType.typeName}")
-    }
-    transaction {
-        NexusBankTransactionEntity.new {
-            bankAccount = NexusBankAccountEntity.findByName(creditorAcct)!!
-            accountTransactionId = "mock"
-            creditDebitIndicator = "CRDT"
-            this.currency = currency
-            this.amount = value
-            status = EntryStatus.BOOK
-            transactionJson = jDetails
-        }
-    }
-}
-
-/**
- * This function generates the Nexus JSON model of one transaction
- * as if it got downloaded via one x-libeufin-bank connection.  The
- * non given values are either resorted from other sources by Nexus,
- * or actually not useful so far.
- */
-private fun genNexusIncomingXLibeufinBank(
-    amount: CurrencyAmount,
-    subject: String
-): XLibeufinBankTransaction =
-    XLibeufinBankTransaction(
-        creditorIban = "NOTUSED",
-        creditorBic =  null,
-        creditorName = "Not Used",
-        debtorIban =  "NOTUSED",
-        debtorBic = null,
-        debtorName = "Not Used",
-        amount = amount.value,
-        currency =  amount.currency,
-        subject =  subject,
-        date = "0",
-        uid =  "not-used",
-        direction = XLibeufinBankDirection.CREDIT
-    )
-/**
- * This function generates the Nexus JSON model of one transaction
- * as if it got downloaded via one Ebics connection.  The non given
- * values are either resorted from other sources by Nexus, or actually
- * not useful so far.
- */
-fun genNexusIncomingCamt(
-    amount: CurrencyAmount,
-    subject: String,
-): CamtBankAccountEntry =
-    CamtBankAccountEntry(
-        amount = amount,
-        creditDebitIndicator = CreditDebitIndicator.CRDT,
-        status = EntryStatus.BOOK,
-        bankTransactionCode = "mock",
-        valueDate = null,
-        bookingDate = null,
-        accountServicerRef = null,
-        entryRef = null,
-        currencyExchange = null,
-        counterValueAmount = null,
-        instructedAmount = null,
-        batches = listOf(
-            Batch(
-                paymentInformationId = null,
-                messageId = null,
-                batchTransactions = listOf(
-                    BatchTransaction(
-                        amount = amount,
-                        creditDebitIndicator = CreditDebitIndicator.CRDT,
-                        details = TransactionDetails(
-                            unstructuredRemittanceInformation = subject,
-                            debtor = PartyIdentification(
-                                name = "Mock Payer",
-                                countryOfResidence = null,
-                                privateId = null,
-                                organizationId = null,
-                                postalAddress = null,
-                                otherId = null
-                            ),
-                            debtorAccount = CashAccount(
-                                iban = "MOCK-IBAN",
-                                name = null,
-                                currency = null,
-                                otherId = null
-                            ),
-                            debtorAgent = AgentIdentification(
-                                bic = "MOCK-BIC",
-                                lei = null,
-                                clearingSystemMemberId = null,
-                                clearingSystemCode = null,
-                                proprietaryClearingSystemCode = null,
-                                postalAddress = null,
-                                otherId = null,
-                                name = null
-                            ),
-                            creditor = null,
-                            creditorAccount = null,
-                            creditorAgent = null,
-                            ultimateCreditor = null,
-                            ultimateDebtor = null,
-                            purpose = null,
-                            proprietaryPurpose = null,
-                            currencyExchange = null,
-                            instructedAmount = null,
-                            counterValueAmount = null,
-                            interBankSettlementAmount = null,
-                            returnInfo = null
-                        )
-                    )
-                )
-            )
-        )
-    )
-
-val poFiCamt054_2019_outgoing: String = """
-    <?xml version="1.0" encoding="UTF-8"?>
-    <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.054.001.08" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.054.001.08 
file:///C:/Users/burkhalterl/Documents/Musterfiles%20ISOV19/Schemen/camt.054.001.08.xsd">
-       <BkToCstmrDbtCdtNtfctn>
-               <GrpHdr>
-                       <MsgId>20200618375204295372463</MsgId>
-                       <CreDtTm>2022-03-10T23:40:14</CreDtTm>
-                       <MsgPgntn>
-                               <PgNb>1</PgNb>
-                               <LastPgInd>true</LastPgInd>
-                       </MsgPgntn>
-                       <AddtlInf>SPS/2.0/PROD</AddtlInf>
-               </GrpHdr>
-               <Ntfctn>
-                       <Id>20200618375204295372465</Id>
-                       <CreDtTm>2022-03-10T23:40:14</CreDtTm>
-                       <FrToDt>
-                               <FrDtTm>2022-03-10T00:00:00</FrDtTm>
-                               <ToDtTm>2022-03-10T23:59:59</ToDtTm>
-                       </FrToDt>
-                       <Acct>
-                               <Id>
-                                       <IBAN>${FOO_USER_IBAN}</IBAN>
-                               </Id>
-                               <Ccy>CHF</Ccy>
-                               <Ownr>
-                                       <Nm>Robert Schneider SA Grands magasins 
Biel/Bienne</Nm>
-                               </Ownr>
-                       </Acct>
-                       <Ntry>
-                               <NtryRef>CH2909000000250094239</NtryRef>
-                               <Amt Ccy="CHF">522.10</Amt>
-                               <CdtDbtInd>DBIT</CdtDbtInd>
-                               <RvslInd>false</RvslInd>
-                               <Sts>
-                                       <Cd>BOOK</Cd>
-                               </Sts>
-                               <BookgDt>
-                                       <Dt>2022-03-10</Dt>
-                               </BookgDt>
-                               <ValDt>
-                                       <Dt>2022-03-10</Dt>
-                               </ValDt>
-                               <AcctSvcrRef>1000000000000000</AcctSvcrRef>
-                               <BkTxCd>
-                                       <Domn>
-                                               <Cd>PMNT</Cd>
-                                               <Fmly>
-                                                       <Cd>RCDT</Cd>
-                                                       
<SubFmlyCd>ATXN</SubFmlyCd>
-                                               </Fmly>
-                                       </Domn>
-                               </BkTxCd>
-                               <NtryDtls>
-                                       <Btch>
-                                               <NbOfTxs>1</NbOfTxs>
-                                       </Btch>
-                                       <TxDtls>
-                                               <Refs>
-                                                       
<InstrId>1006265-25bbb3b1a</InstrId>
-                                                       
<EndToEndId>client-generated</EndToEndId>
-                                                       
<UETR>b009c997-97b3-4a9c-803c-d645a7276bf0</UETR>
-                                                       <Prtry>
-                                                               <Tp>00</Tp>
-                                                               
<Ref>00000000000000000000020</Ref>
-                                                       </Prtry>
-                                               </Refs>
-                                               <Amt Ccy="CHF">522.10</Amt>
-                                               <CdtDbtInd>DBIT</CdtDbtInd>
-                                               <BkTxCd>
-                                                       <Domn>
-                                                               <Cd>PMNT</Cd>
-                                                               <Fmly>
-                                                                       
<Cd>RCDT</Cd>
-                                                                       
<SubFmlyCd>ATXN</SubFmlyCd>
-                                                               </Fmly>
-                                                       </Domn>
-                                               </BkTxCd>
-                                               <RltdPties>
-                                                       <Dbtr>
-                                                               <Pty>
-                                                                       
<Nm>Bernasconi Maria</Nm>
-                                                                       
<PstlAdr>
-                                                                               
<AdrLine>Place de la Gare 12</AdrLine>
-                                                                               
<AdrLine>2502 Biel/Bienne</AdrLine>
-                                                                       
</PstlAdr>
-                                                               </Pty>
-                                                       </Dbtr>
-                                                       <DbtrAcct>
-                                                               <Id>
-                                                                       
<IBAN>CH5109000000250092291</IBAN>
-                                                               </Id>
-                                                       </DbtrAcct>
-                                                       <CdtrAcct>
-                                                               <Id>
-                                                                       
<IBAN>CH2909000000250094239</IBAN>
-                                                               </Id>
-                                                       </CdtrAcct>
-                                               </RltdPties>
-                                               <RltdAgts>
-                                                       <DbtrAgt>
-                                                               <FinInstnId>
-                                                                       
<BICFI>POFICHBEXXX</BICFI>
-                                                                       
<Nm>POSTFINANCE AG</Nm>
-                                                                       
<PstlAdr>
-                                                                               
<AdrLine>MINGERSTRASSE 20</AdrLine>
-                                                                               
<AdrLine>3030 BERNE</AdrLine>
-                                                                       
</PstlAdr>
-                                                               </FinInstnId>
-                                                       </DbtrAgt>
-                                               </RltdAgts>
-                                               <RmtInf>
-                                                       <Strd>
-                                                               
<AddtlRmtInf>?REJECT?0</AddtlRmtInf>
-                                                               
<AddtlRmtInf>?ERROR?000</AddtlRmtInf>
-                                                       </Strd>
-                                <Ustrd>Reserve pub.</Ustrd>
-                                               </RmtInf>
-                                               <RltdDts>
-                                                       
<AccptncDtTm>2022-03-10T20:00:00</AccptncDtTm>
-                                               </RltdDts>
-                                       </TxDtls>
-                               </NtryDtls>
-                               <AddtlNtryInf>GUTSCHRIFT AUFTRAGGEBER: 
Bernasconi Maria Place de la Gare 12  2502 Biel/Bienne REFERENZEN: NOTPROVIDED 
1006265-25bbb3b1a 2000000000000000</AddtlNtryInf>
-                       </Ntry>
-               </Ntfctn>
-       </BkToCstmrDbtCdtNtfctn>
-    </Document>
-""".trimIndent()
-
-// Comes from a "mit Sammelbuchung" sample.
-// "mit Einzelbuchung" sample didn't have the "Ustrd"
-// See: 
https://www.postfinance.ch/de/support/services/dokumente/musterfiles-fuer-geschaeftskunden.html
-val poFiCamt054_2019_incoming: String = """
-<?xml version="1.0" encoding="UTF-8"?>
-<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.054.001.08" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.054.001.08 
file:///C:/Users/burkhalterl/Documents/Musterfiles%20ISOV19/Schemen/camt.054.001.08.xsd">
-       <BkToCstmrDbtCdtNtfctn>
-               <GrpHdr>
-                       <MsgId>20200618375204295372463</MsgId>
-                       <CreDtTm>2022-03-08T23:31:31</CreDtTm>
-                       <MsgPgntn>
-                               <PgNb>1</PgNb>
-                               <LastPgInd>true</LastPgInd>
-                       </MsgPgntn>
-                       <AddtlInf>SPS/2.0/PROD</AddtlInf>
-               </GrpHdr>
-               <Ntfctn>
-                       <Id>20200618375204295372465</Id>
-                       <CreDtTm>2022-03-08T23:31:31</CreDtTm>
-                       <FrToDt>
-                               <FrDtTm>2022-03-08T00:00:00</FrDtTm>
-                               <ToDtTm>2022-03-08T23:59:59</ToDtTm>
-                       </FrToDt>
-                       <Acct>
-                               <Id>
-                                       <IBAN>${FOO_USER_IBAN}</IBAN>
-                               </Id>
-                               <Ccy>CHF</Ccy>
-                               <Ownr>
-                                       <Nm>Robert Schneider SA Grands magasins 
Biel/Bienne</Nm>
-                               </Ownr>
-                       </Acct>
-                       <Ntry>
-                               <NtryRef>CH2909000000250094239</NtryRef>
-                               <Amt Ccy="CHF">501.05</Amt>
-                               <CdtDbtInd>CRDT</CdtDbtInd>
-                               <RvslInd>false</RvslInd>
-                               <Sts>
-                                       <Cd>BOOK</Cd>
-                               </Sts>
-                               <BookgDt>
-                                       <Dt>2022-03-08</Dt>
-                               </BookgDt>
-                               <ValDt>
-                                       <Dt>2022-03-08</Dt>
-                               </ValDt>
-                               <AcctSvcrRef>1000000000000000</AcctSvcrRef>
-                               <BkTxCd>
-                                       <Domn>
-                                               <Cd>PMNT</Cd>
-                                               <Fmly>
-                                                       <Cd>RCDT</Cd>
-                                                       
<SubFmlyCd>AUTT</SubFmlyCd>
-                                               </Fmly>
-                                       </Domn>
-                               </BkTxCd>
-                               <NtryDtls>
-                                       <Btch>
-                                               <NbOfTxs>1</NbOfTxs>
-                                       </Btch>
-                                       <TxDtls>
-                                               <Refs>
-                                                       
<AcctSvcrRef>2000000000000000</AcctSvcrRef>
-                                                       
<InstrId>1006265-25bbb3b1a</InstrId>
-                                                       
<EndToEndId>NOTPROVIDED</EndToEndId>
-                                                       
<UETR>b009c997-97b3-4a9c-803c-d645a7276b0</UETR>
-                                                       <Prtry>
-                                                               <Tp>00</Tp>
-                                                               
<Ref>00000000000000000000020</Ref>
-                                                       </Prtry>
-                                               </Refs>
-                                               <Amt Ccy="CHF">501.05</Amt>
-                                               <CdtDbtInd>CRDT</CdtDbtInd>
-                                               <BkTxCd>
-                                                       <Domn>
-                                                               <Cd>PMNT</Cd>
-                                                               <Fmly>
-                                                                       
<Cd>RCDT</Cd>
-                                                                       
<SubFmlyCd>AUTT</SubFmlyCd>
-                                                               </Fmly>
-                                                       </Domn>
-                                               </BkTxCd>
-                                               <RltdPties>
-                                                       <Dbtr>
-                                                               <Pty>
-                                                                       
<Nm>Bernasconi Maria</Nm>
-                                                                       
<PstlAdr>
-                                                                               
<AdrLine>Place de la Gare 12</AdrLine>
-                                                                               
<AdrLine>2502 Biel/Bienne</AdrLine>
-                                                                       
</PstlAdr>
-                                                               </Pty>
-                                                       </Dbtr>
-                                                       <DbtrAcct>
-                                                               <Id>
-                                                                       
<IBAN>CH5109000000250092291</IBAN>
-                                                               </Id>
-                                                       </DbtrAcct>
-                                                       <CdtrAcct>
-                                                               <Id>
-                                                                       
<IBAN>CH2909000000250094239</IBAN>
-                                                               </Id>
-                                                       </CdtrAcct>
-                                               </RltdPties>
-                                               <RltdAgts>
-                                                       <DbtrAgt>
-                                                               <FinInstnId>
-                                                                       
<BICFI>POFICHBEXXX</BICFI>
-                                                                       
<Nm>POSTFINANCE AG</Nm>
-                                                                       
<PstlAdr>
-                                                                               
<AdrLine>MINGERSTRASSE , 20</AdrLine>
-                                                                               
<AdrLine>3030 BERN</AdrLine>
-                                                                       
</PstlAdr>
-                                                               </FinInstnId>
-                                                       </DbtrAgt>
-                                               </RltdAgts>
-                                               <RmtInf>
-                                                       <Ustrd>Muster</Ustrd>
-                                                       <Ustrd> 
Musterfile</Ustrd>
-                                                       <Strd>
-                                                               
<AddtlRmtInf>?REJECT?0</AddtlRmtInf>
-                                                               
<AddtlRmtInf>?ERROR?000</AddtlRmtInf>
-                                                       </Strd>
-                                               </RmtInf>
-                                               <RltdDts>
-                                                       
<AccptncDtTm>2022-03-08T20:00:00</AccptncDtTm>
-                                               </RltdDts>
-                                       </TxDtls>
-                               </NtryDtls>
-                               <AddtlNtryInf>SAMMELGUTSCHRIFT FÜR KONTO: 
CH2909000000250094239 VERARBEITUNG VOM 08.03.2022 PAKET ID: 
200000000000XXX</AddtlNtryInf>
-                       </Ntry>
-               </Ntfctn>
-       </BkToCstmrDbtCdtNtfctn>
-</Document>
-""".trimIndent()
-
-// Abstracts the mock handler installation.
-fun getMockedClient(handler: MockRequestHandleScope.(HttpRequestData) -> 
HttpResponseData): HttpClient {
-    return HttpClient(MockEngine) {
-        followRedirects = false
-        engine {
-            addHandler {
-                    request -> handler(request)
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/NexusApiTest.kt 
b/nexus/src/test/kotlin/NexusApiTest.kt
deleted file mode 100644
index 01ef9565..00000000
--- a/nexus/src/test/kotlin/NexusApiTest.kt
+++ /dev/null
@@ -1,272 +0,0 @@
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.client.engine.mock.*
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.config.*
-import io.ktor.server.testing.*
-import io.netty.handler.codec.http.HttpResponseStatus
-import kotlinx.coroutines.async
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.ensureActive
-import kotlinx.coroutines.runBlocking
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Test
-import tech.libeufin.nexus.PaymentInitiationEntity
-import tech.libeufin.nexus.bankaccount.ingestBankMessagesIntoAccount
-import tech.libeufin.nexus.getConnectionPlugin
-import tech.libeufin.nexus.iso20022.ingestCamtMessageIntoAccount
-import tech.libeufin.nexus.server.*
-import tech.libeufin.sandbox.BankAccountTransactionEntity
-import tech.libeufin.sandbox.BankAccountTransactionsTable
-import tech.libeufin.sandbox.sandboxApp
-import tech.libeufin.sandbox.wireTransfer
-
-/**
- * This class tests the API offered by Nexus,
- * documented here: https://docs.taler.net/libeufin/api-nexus.html
- */
-class NexusApiTest {
-    private val jMapper = ObjectMapper()
-    // Testing long-polling on GET /transactions
-    @Test
-    fun getTransactions() {
-        withTestDatabase {
-            prepNexusDb()
-            testApplication {
-                application(nexusApp)
-                /**
-                 * Requesting /transactions with long polling, and assert that
-                 * the response arrives _after_ the unblocking INSERT into the
-                 * database.
-                 */
-                val longPollMs = 5000
-                runBlocking {
-                    val requestJob = async {
-                        
client.get("/bank-accounts/foo/transactions?long_poll_ms=$longPollMs") {
-                            basicAuth("foo", "foo")
-                            contentType(ContentType.Application.Json)
-                        }
-                    }
-                    /**
-                     * The following delay ensures that the payment below
-                     * gets inserted after the client has issued the long
-                     * polled request above (and it is therefore waiting)
-                     */
-                    delay(2000)
-                    // Ensures that the request is active _before_ the
-                    // upcoming payment.  This ensures that the request
-                    // didn't find already another payment in the database.
-                    requestJob.ensureActive()
-                    newNexusBankTransaction(
-                        currency = "TESTKUDOS",
-                        value = "2",
-                        subject = "first"
-                    )
-                    val R = requestJob.await()
-                    // Ensures that the request did NOT wait all the timeout
-                    assert((R.responseTime.timestamp - 
R.requestTime.timestamp) < longPollMs)
-                    val body = jacksonObjectMapper().readTree(R.bodyAsText())
-                    // Ensures that the unblocking payment exists in the 
response.
-                    val tx = body.get("transactions")
-                    assert(tx.isArray && tx.size() == 1)
-                }
-            }
-        }
-    }
-    @Test
-    fun facadeIdempotence() {
-        val facadeData = """{
-          "name": "foo-facade",
-          "type": "taler-wire-gateway",
-          "config": {
-            "bankAccount": "foo",
-            "bankConnection": "foo",
-            "reserveTransferLevel": "report",
-            "currency": "TESTKUDOS"
-          }
-        }""".trimIndent()
-        withTestDatabase {
-            prepNexusDb()
-            testApplication {
-                application(nexusApp)
-                client.post("/facades") {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                    contentType(ContentType.Application.Json)
-                    setBody(facadeData)
-                }
-                // Changing one detail, and expecting 409 Conflict.
-                var resp = client.post("/facades") {
-                    expectSuccess = false
-                    basicAuth("foo", "foo")
-                    contentType(ContentType.Application.Json)
-                    setBody(facadeData.replace(
-                        "taler-wire-gateway",
-                        "anastasis"
-                    ))
-                }
-                assert(resp.status.value == HttpStatusCode.Conflict.value)
-                // Changing a value deeper in the request object.
-                resp = client.post("/facades") {
-                    expectSuccess = false
-                    basicAuth("foo", "foo")
-                    contentType(ContentType.Application.Json)
-                    setBody(facadeData.replace(
-                        "report",
-                        "statement"
-                    ))
-                }
-                assert(resp.status.value == HttpStatusCode.Conflict.value)
-            }
-        }
-    }
-    // Testing basic operations on facades.
-    @Test
-    fun facades() {
-        // Deletes the facade (created previously by MakeEnv.kt)
-        withTestDatabase {
-            prepNexusDb()
-            testApplication {
-                application(nexusApp)
-                client.delete("/facades/foo-facade") {
-                    basicAuth("foo", "foo")
-                    expectSuccess = true
-                }
-            }
-        }
-    }
-
-    // Testing the creation of scheduled tasks.
-    @Test
-    fun schedule() {
-        withTestDatabase {
-            prepNexusDb()
-            testApplication {
-                application(nexusApp)
-                // POSTing omitted 'params', to test whether Nexus
-                // expects it as 'null' for a 'submit' task.
-                client.post("/bank-accounts/foo/schedule") {
-                    contentType(ContentType.Application.Json)
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                    setBody("""{
-                        "name": "send-payments",
-                        "cronspec": "* * *",
-                        "type": "submit",
-                        "params": null
-                    }""".trimIndent())
-                }
-            }
-        }
-    }
-    /**
-     * Testing the idempotence of payment submissions.  That
-     * helps Sandbox not to create multiple payment initiations
-     * in case it fails at keeping track of what it submitted
-     * already.
-     */
-    @Test
-    fun paymentInitIdempotence() {
-        withTestDatabase {
-            prepNexusDb()
-            testApplication {
-                application(nexusApp)
-                // Check no pay. ini. exist.
-                transaction { PaymentInitiationEntity.all().count() == 0L }
-                // Create one.
-                fun f(futureThis: HttpRequestBuilder, subject: String = 
"idempotence pay. init. test") {
-                    futureThis.basicAuth("foo", "foo")
-                    futureThis.expectSuccess = true
-                    futureThis.contentType(ContentType.Application.Json)
-                    futureThis.setBody("""
-                        {"iban": "TESTIBAN",
-                         "bic": "SANDBOXX",
-                         "name": "TEST NAME",
-                         "amount": "TESTKUDOS:3",
-                         "subject": "$subject",
-                         "uid": "salt"
-                         }
-                    """.trimIndent())
-                }
-                val R = client.post("/bank-accounts/foo/payment-initiations") 
{ f(this) }
-                println(jMapper.readTree(R.bodyAsText()).get("uuid"))
-                // Submit again
-                client.post("/bank-accounts/foo/payment-initiations") { 
f(this) }
-                // Checking that Nexus serves it.
-                client.get("/bank-accounts/foo/payment-initiations/1") {
-                    basicAuth("foo", "foo")
-                    expectSuccess = true
-                }
-                // Checking that the database has only one, despite the double 
submission.
-                transaction {
-                    assert(PaymentInitiationEntity.all().count() == 1L)
-                }
-                /**
-                 * Causing a conflict by changing one payment detail
-                 * (the subject in this case) but not the "uid".
-                 */
-                val maybeConflict = 
client.post("/bank-accounts/foo/payment-initiations") {
-                    f(this, "different-subject")
-                    expectSuccess = false
-                }
-                assert(maybeConflict.status.value == 
HttpStatusCode.Conflict.value)
-            }
-        }
-    }
-    @Test
-    fun timeRangeFetch() {
-        withTestDatabase {
-            prepSandboxDb()
-            prepNexusDb()
-            val ref = wireTransfer(
-                "admin",
-                "foo",
-                subject = "past payment",
-                amount = "TESTKUDOS:30"
-                )
-            transaction {
-                BankAccountTransactionEntity.find {
-                    BankAccountTransactionsTable.accountServicerReference eq 
ref
-                }.first().date = 1577833200000L // Jan, 1st, 2020
-            }
-            testApplication {
-                application(sandboxApp)
-                val conn = getConnectionPlugin("ebics")
-
-                // Asking a time range where the one payment is expected to 
exist
-                conn.fetchTransactions(
-                    fetchSpec = FetchSpecTimeRangeJson(
-                        FetchLevel.REPORT,
-                        start = "2019-12-31",
-                        end = "2020-01-02",
-                        bankConnection = null
-                    ),
-                    accountId = "foo",
-                    bankConnectionId = "foo",
-                    client = client
-                )
-                val res = ingestBankMessagesIntoAccount("foo", "foo")
-                assert(res.newTransactions == 1)
-                // Asking a time range where the one payment is NOT expected 
to exist
-                conn.fetchTransactions(
-                    fetchSpec = FetchSpecTimeRangeJson(
-                        FetchLevel.REPORT,
-                        start = "2019-10-31",
-                        end = "2019-11-30",
-                        bankConnection = null
-                    ),
-                    accountId = "foo",
-                    bankConnectionId = "foo",
-                    client = client
-                )
-                val resNoData = ingestBankMessagesIntoAccount("foo", "foo")
-                assert(resNoData.downloadedTransactions == 0)
-                assert(resNoData.newTransactions == 0)
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/PainTest.kt 
b/nexus/src/test/kotlin/PainTest.kt
deleted file mode 100644
index 33140cbc..00000000
--- a/nexus/src/test/kotlin/PainTest.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-import ch.qos.logback.core.joran.spi.XMLUtil
-import org.junit.Test
-import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData
-import tech.libeufin.nexus.iso20022.createPain001document
-import kotlin.test.assertTrue
-
-class PainTest {
-
-    @Test
-    fun validationTest() {
-        val xml = createPain001document(
-            NexusPaymentInitiationData(
-                debtorIban = "GB33BUKB20201222222222",
-                debtorBic = "BUKBGB33",
-                debtorName = "Oliver Smith",
-                currency = "EUR",
-                amount = "1",
-                creditorIban = "GB33BUKB20201222222222",
-                creditorName = "Oliver Smith",
-                messageId = "message id",
-                paymentInformationId = "payment information id",
-                preparationTimestamp = 0,
-                subject = "subject",
-                instructionId = "instruction id",
-                endToEndId = "end to end id",
-                creditorBic = "BUKBGB33"
-            )
-        )
-        assertTrue {
-            tech.libeufin.util.XMLUtil.validateFromString(xml)
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/PostFinance.kt 
b/nexus/src/test/kotlin/PostFinance.kt
deleted file mode 100644
index 8c392487..00000000
--- a/nexus/src/test/kotlin/PostFinance.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import com.github.ajalt.clikt.core.*
-import com.github.ajalt.clikt.parameters.options.default
-import com.github.ajalt.clikt.parameters.options.option
-import io.ktor.client.*
-import kotlinx.coroutines.runBlocking
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.bankaccount.addPaymentInitiation
-import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
-import tech.libeufin.nexus.ebics.EbicsUploadSpec
-import tech.libeufin.nexus.ebics.doEbicsUploadTransaction
-import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails
-import tech.libeufin.nexus.getBankAccount
-import tech.libeufin.nexus.getBankConnection
-import tech.libeufin.nexus.getConnectionPlugin
-import tech.libeufin.nexus.getNexusUser
-import tech.libeufin.nexus.server.*
-import tech.libeufin.util.ebics_h005.Ebics3Request
-import java.io.BufferedReader
-import java.io.File
-import kotlin.system.exitProcess
-
-// Asks a camt.054 to the bank.
-private fun downloadPayments() {
-    val httpClient = HttpClient()
-    runBlocking {
-        fetchBankAccountTransactions(
-            client = httpClient,
-            fetchSpec = FetchSpecLatestJson(
-                level = FetchLevel.NOTIFICATION,
-                bankConnection = null
-            ),
-            accountId = "foo"
-        )
-    }
-}
-
-/* Simulates one incoming payment for the 'payee' argument.
- * It pays the test platform's bank account if none is found.
- * The QRR format is NOT used in Taler, it is just convenient.
- * */
-private fun uploadQrrPayment(maybePayee: String? = null) {
-    val payee = if (maybePayee == null) {
-        val localAccount = getBankAccount("foo")
-        localAccount.iban
-    } else maybePayee
-    val httpClient = HttpClient()
-    val qrr = """
-        
Product;Channel;Account;Currency;Amount;Reference;Name;Street;Number;Postcode;City;Country;DebtorAddressLine;DebtorAddressLine;DebtorAccount;ReferenceType;UltimateDebtorName;UltimateDebtorStreet;UltimateDebtorNumber;UltimateDebtorPostcode;UltimateDebtorTownName;UltimateDebtorCountry;UltimateDebtorAddressLine;UltimateDebtorAddressLine;RemittanceInformationText
-        
QRR;PO;$payee;CHF;33;;D009;Musterstrasse;1;1111;Musterstadt;CH;;;;NON;D009;Musterstrasse;1;1111;Musterstadt;CH;;;Taler-Demo
-    """.trimIndent()
-    runBlocking {
-        doEbicsUploadTransaction(
-            httpClient,
-            getEbicsSubscriberDetails("postfinance"),
-            EbicsUploadSpec(
-                ebics3Service = Ebics3Request.OrderDetails.Service().apply {
-                    serviceName = "OTH"
-                    scope = "BIL"
-                    serviceOption = "CH002LMF"
-                    messageName = 
Ebics3Request.OrderDetails.Service.MessageName().apply {
-                        value = "csv"
-                    }
-                },
-                isEbics3 = true
-            ),
-            qrr.toByteArray(Charsets.UTF_8)
-        )
-    }
-}
-
-/**
- * Submits a pain.001 version 2019 message to the bank.
- *
- * Causes one DBIT payment to show up in the camt.054.  This one
- * however lacks the AcctSvcrRef, so other ways to pin it are needed.
- * Notably, EndToEndId is mandatory in pain.001 _and_ is controlled
- * by the sender.  Hence, the sender can itself ensure the EndToEndId
- * uniqueness.
- */
-private fun uploadPain001Payment(
-    subject: String,
-    creditorIban: String = "CH9300762011623852957" // random creditor
-) {
-    transaction {
-        addPaymentInitiation(
-            Pain001Data(
-                creditorIban = creditorIban,
-                creditorBic = "POFICHBEXXX",
-                creditorName = "Muster Frau",
-                sum = "2",
-                currency = "CHF",
-                subject = subject,
-                endToEndId = "Zufall"
-            ),
-            getBankAccount("foo").bankAccountName
-        )
-    }
-    val ebicsConn = getConnectionPlugin("ebics")
-    val httpClient = HttpClient()
-    runBlocking { ebicsConn.submitPaymentInitiation(httpClient, 1L) }
-}
-
-class PostFinanceCommand : CliktCommand() {
-    private val myIban by option(
-        help = "IBAN as assigned by the PostFinance test platform."
-    ).default("CH9789144829733648596")
-    override fun run() { prepare(myIban) }
-}
-class Download : CliktCommand("Download the latest camt.054 from the bank") {
-    // Ask 'notification' to the bank.
-    override fun run() {
-        // uploadPain001Payment("auto")
-        downloadPayments()
-    }
-}
-
-class Upload : CliktCommand("Upload a pain.001 to the bank") {
-    private val subject by option(help = "Payment subject").default("Muster 
Zahlung")
-    override fun run() { uploadPain001Payment(subject) }
-}
-
-class GenIncoming : CliktCommand("Uploads a CSV document to create one 
incoming payment") {
-    override fun run() {
-        val bankAccount = getBankAccount("foo")
-        uploadQrrPayment(bankAccount.iban)
-    }
-}
-
-private fun prepare(iban: String) {
-    // Loads EBICS subscriber's keys from disk.
-    // The keys should be found under libeufin-internal.git/convenience/
-    val bufferedReader: BufferedReader = 
File("/tmp/pofi.json").bufferedReader()
-    val accessDataTxt = bufferedReader.use { it.readText() }
-    val ebicsConn = getConnectionPlugin("ebics")
-    val accessDataJson = jacksonObjectMapper().readTree(accessDataTxt)
-
-    // Creates a connection handle to the bank, using the loaded keys.
-    withTestDatabase {
-        prepNexusDb()
-        transaction {
-            ebicsConn.createConnectionFromBackup(
-                connId = "postfinance",
-                user = getNexusUser("foo"),
-                passphrase = "secret",
-                accessDataJson
-            )
-            val fooBankAccount = getBankAccount("foo")
-            // Hooks the PoFi details to the local bank account.
-            // No need to run the canonical steps (creating account, 
downloading bank accounts, ..)
-            fooBankAccount.defaultBankConnection = 
getBankConnection("postfinance")
-            fooBankAccount.iban = iban
-        }
-    }
-}
-fun main(args: Array<String>) {
-    PostFinanceCommand().subcommands(Download(), Upload(), 
GenIncoming()).main(args)
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/SandboxAccessApiTest.kt 
b/nexus/src/test/kotlin/SandboxAccessApiTest.kt
deleted file mode 100644
index 4236e0ce..00000000
--- a/nexus/src/test/kotlin/SandboxAccessApiTest.kt
+++ /dev/null
@@ -1,491 +0,0 @@
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.ktor.server.testing.*
-import io.ktor.util.*
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.time.delay
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Ignore
-import org.junit.Test
-import tech.libeufin.sandbox.*
-import tech.libeufin.util.getDatabaseName
-import java.util.*
-import kotlin.concurrent.schedule
-
-class SandboxAccessApiTest {
-    val mapper = ObjectMapper()
-    private fun getTxs(respJson: String): JsonNode {
-        val mapper = ObjectMapper()
-        return mapper.readTree(respJson).get("transactions")
-    }
-
-    /**
-     * Testing that ..access-api/withdrawals/{wopid} and
-     * ..access-api/accounts/{account_name}/withdrawals/{wopid}
-     * are handled in the same way.
-     */
-    @Test
-    fun doubleUriStyle() {
-        // Creating one withdrawal operation.
-        withTestDatabase {
-            prepSandboxDb()
-            val wo: TalerWithdrawalEntity = transaction {
-                TalerWithdrawalEntity.new {
-                    this.amount = "TESTKUDOS:3.3"
-                    walletBankAccount = getBankAccountFromLabel("foo")
-                    selectedExchangePayto = 
"payto://iban/SANDBOXX/${BAR_USER_IBAN}"
-                    reservePub = "not used"
-                    selectionDone = true
-                }
-            }
-            testApplication {
-                application(sandboxApp)
-                // Showing withdrawal info.
-                val get_with_account = 
client.get("/demobanks/default/access-api/accounts/foo/withdrawals/${wo.wopid}")
 {
-                    expectSuccess = true
-                }
-                val get_without_account = 
client.get("/demobanks/default/access-api/withdrawals/${wo.wopid}") {
-                    expectSuccess = true
-                }
-                assert(get_without_account.bodyAsText() == 
get_with_account.bodyAsText())
-                assert(get_with_account.bodyAsText() == 
get_without_account.bodyAsText())
-                // Confirming a withdrawal.
-                val confirm_with_account = 
client.post("/demobanks/default/access-api/accounts/foo/withdrawals/${wo.wopid}/confirm")
 {
-                    expectSuccess = true
-                }
-                val confirm_without_account = 
client.post("/demobanks/default/access-api/withdrawals/${wo.wopid}/confirm") {
-                    expectSuccess = true
-                }
-                assert(confirm_with_account.status.value == 
confirm_without_account.status.value)
-                assert(confirm_with_account.bodyAsText() == 
confirm_without_account.bodyAsText())
-                // Aborting one withdrawal.
-                var wo_to_abort = transaction {
-                    TalerWithdrawalEntity.new {
-                        this.amount = "TESTKUDOS:3.3"
-                        walletBankAccount = getBankAccountFromLabel("foo")
-                        selectedExchangePayto = 
"payto://iban/SANDBOXX/${BAR_USER_IBAN}"
-                        reservePub = "not used"
-                        selectionDone = true
-                    }
-                }
-                val abort_with_account = 
client.post("/demobanks/default/access-api/accounts/foo/withdrawals/${wo_to_abort.wopid}/abort")
 {
-                    expectSuccess = true
-                }
-                wo_to_abort = transaction {
-                    TalerWithdrawalEntity.new {
-                        this.amount = "TESTKUDOS:3.3"
-                        walletBankAccount = getBankAccountFromLabel("foo")
-                        selectedExchangePayto = 
"payto://iban/SANDBOXX/${BAR_USER_IBAN}"
-                        reservePub = "not used"
-                        selectionDone = true
-                    }
-                }
-                val abort_without_account = 
client.post("/demobanks/default/access-api/withdrawals/${wo_to_abort.wopid}/abort")
 {
-                    expectSuccess = true
-                }
-                assert(abort_with_account.status.value == 
abort_without_account.status.value)
-                // Not checking the content as they abort two different 
operations.
-            }
-        }
-    }
-
-    // Move funds between accounts.
-    @Test
-    fun wireTransfer() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    // Foo gives 20 to Bar
-                    
client.post("/demobanks/default/access-api/accounts/foo/transactions") {
-                        expectSuccess = true
-                        contentType(ContentType.Application.Json)
-                        basicAuth("foo", "foo")
-                        setBody("""{
-                            "paytoUri": 
"payto://iban/${BAR_USER_IBAN}?message=test",
-                            "amount": "TESTKUDOS:20"
-                        }""".trimIndent()
-                        )
-                    }
-                    // Foo checks its balance: -20
-                    var R = 
client.get("/demobanks/default/access-api/accounts/foo") {
-                        basicAuth("foo", "foo")
-                    }
-                    val mapper = ObjectMapper()
-                    var j = mapper.readTree(R.readBytes())
-                    val expectDebitOf20 = 
j.get("balance").get("amount").asText()
-                    println("Expect debit of 20: $expectDebitOf20")
-                    val testkudos20regex = "^TESTKUDOS:20(.00)?$".toRegex()
-                    assert(testkudos20regex.matches(expectDebitOf20))
-                    
assert(j.get("balance").get("credit_debit_indicator").asText().lowercase() == 
"debit")
-                    // Bar checks its balance: 20
-                    R = 
client.get("/demobanks/default/access-api/accounts/bar") {
-                        basicAuth("bar", "bar")
-                    }
-                    j = mapper.readTree(R.readBytes())
-                    
assert(testkudos20regex.matches(j.get("balance").get("amount").asText()))
-                    
assert(j.get("balance").get("credit_debit_indicator").asText().lowercase() == 
"credit")
-                    // Foo tries with an invalid amount
-                    R = 
client.post("/demobanks/default/access-api/accounts/foo/transactions") {
-                        contentType(ContentType.Application.Json)
-                        basicAuth("foo", "foo")
-                        setBody("""{
-                            "paytoUri": 
"payto://iban/${BAR_USER_IBAN}?message=test",
-                            "amount": "TESTKUDOS:20.001"
-                        }""".trimIndent()
-                        )
-                    }
-                    assert(R.status.value == HttpStatusCode.BadRequest.value)
-                }
-            }
-        }
-    }
-
-    // Tests the time range filter of Access API's GET /transactions
-    @Test
-    fun timeRangedTransactions() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                var R = 
client.get("/demobanks/default/access-api/accounts/foo/transactions") {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                assert(getTxs(R.bodyAsText()).size() == 0) // Checking that no 
transactions exist.
-                wireTransfer(
-                    "admin",
-                    "foo",
-                    "default",
-                    "#0",
-                    "TESTKUDOS:2"
-                )
-                R = 
client.get("/demobanks/default/access-api/accounts/foo/transactions") {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                assert(getTxs(R.bodyAsText()).size() == 1) // Checking that #0 
shows up.
-                // Asking up to a point in the past, where no txs should exist.
-                R = 
client.get("/demobanks/default/access-api/accounts/foo/transactions?until_ms=3000")
 {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                assert(getTxs(R.bodyAsText()).size() == 0) // Not expecting 
any transaction.
-                // Moving the transaction back in the past
-                transaction {
-                    val tx_0 = BankAccountTransactionEntity.find {
-                        BankAccountTransactionsTable.subject eq "#0" and
-                                (BankAccountTransactionsTable.direction eq 
"CRDT")
-                    }.first()
-                    tx_0.date = 10000
-                }
-                // Picking the past transaction from one including time range,
-                // therefore expecting one entry in the result
-                R = 
client.get("/demobanks/default/access-api/accounts/foo/transactions?from_ms=9000&until_ms=11000")
 {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                assert(getTxs(R.bodyAsText()).size() == 1)
-                // Not enough txs to fill the second page, expecting no txs 
therefore.
-                R = 
client.get("/demobanks/default/access-api/accounts/foo/transactions?page=2&size=1")
 {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                assert(getTxs(R.bodyAsText()).size() == 0)
-                // Creating one more tx and asking the second 1-sized page, 
expecting therefore one result.
-                wireTransfer(
-                    "admin",
-                    "foo",
-                    "default",
-                    "#1",
-                    "TESTKUDOS:2"
-                )
-                R = 
client.get("/demobanks/default/access-api/accounts/foo/transactions?page=2&size=1")
 {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                assert(getTxs(R.bodyAsText()).size() == 1)
-            }
-        }
-    }
-
-    // Tests for #7482
-    @Test
-    fun highAmountWithdraw() {
-        withTestDatabase {
-            prepSandboxDb(usersDebtLimit = 900000000)
-            testApplication {
-                application(sandboxApp)
-                // Create the operation.
-                val r = 
client.post("/demobanks/default/access-api/accounts/foo/withdrawals") {
-                    expectSuccess = true
-                    setBody("{\"amount\": \"TESTKUDOS:500000000\"}")
-                    contentType(ContentType.Application.Json)
-                    basicAuth("foo", "foo")
-                }
-                println(r.bodyAsText())
-                val j = mapper.readTree(r.readBytes())
-                val op = j.get("withdrawal_id").asText()
-                // Select exchange and specify a reserve pub.
-                
client.post("/demobanks/default/integration-api/withdrawal-operation/$op") {
-                    expectSuccess = true
-                    contentType(ContentType.Application.Json)
-                    setBody("""{
-                        "selected_exchange":"payto://iban/${BAR_USER_IBAN}",
-                        "reserve_pub": "not-used"
-                    }""".trimIndent())
-                }
-                // Confirm the operation.
-                
client.post("/demobanks/default/access-api/accounts/foo/withdrawals/$op/confirm")
 {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                // Check the withdrawal amount in the unique transaction.
-                val t = 
client.get("/demobanks/default/access-api/accounts/foo/transactions") {
-                    basicAuth("foo", "foo")
-                    expectSuccess = true
-                }
-                println(t.bodyAsText())
-                val amount = 
mapper.readTree(t.readBytes()).get("transactions").get(0).get("amount").asText()
-                assert(amount == "500000000")
-            }
-        }
-    }
-    @Test
-    fun withdrawWithHighBalance() {
-        withTestDatabase {
-            prepSandboxDb()
-            /**
-             * A problem appeared (Sandbox responding "insufficient funds")
-             * when B - A > T, where B is the balance, A the potential amount
-             * to withdraw and T is the debit threshold for the user.  T is
-             * 1000 here, therefore setting B as 2000 and A as 1 should get
-             * this case tested.
-             */
-            wireTransfer(
-                "admin",
-                "foo",
-                "default",
-                "bring balance to high amount",
-                "TESTKUDOS:2000"
-            )
-            testApplication {
-                this.application(sandboxApp)
-                runBlocking {
-                    
client.post("/demobanks/default/access-api/accounts/foo/withdrawals") {
-                        expectSuccess = true
-                        setBody("{\"amount\": \"TESTKUDOS:1\"}")
-                        contentType(ContentType.Application.Json)
-                        basicAuth("foo", "foo")
-                    }
-                }
-            }
-        }
-    }
-    // Check successful and failing case due to insufficient funds.
-    @Test
-    fun debitWithdraw() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                this.application(sandboxApp)
-                runBlocking {
-                    // Normal, successful withdrawal.
-                    
client.post("/demobanks/default/access-api/accounts/foo/withdrawals") {
-                        expectSuccess = true
-                        setBody("{\"amount\": \"TESTKUDOS:1\"}")
-                        contentType(ContentType.Application.Json)
-                        basicAuth("foo", "foo")
-                    }
-                    // Withdrawal over the debit threshold.
-                    val r: HttpResponse = 
client.post("/demobanks/default/access-api/accounts/foo/withdrawals") {
-                        expectSuccess = false
-                        contentType(ContentType.Application.Json)
-                        basicAuth("foo", "foo")
-                        setBody("{\"amount\": \"TESTKUDOS:99999999999\"}")
-                    }
-                    assert(HttpStatusCode.Conflict.value == r.status.value)
-                }
-            }
-        }
-    }
-
-    /**
-     * Tests that 'admin' and 'bank' are not possible to register
-     * and that after 'admin' logs in it gets access to the bank's
-     * main account.
-     */ // FIXME: avoid giving Content-Type at every request.
-    @Test
-    fun adminRegisterAndLoginTest() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    val registerAdmin = mapper.writeValueAsString(object {
-                        val username = "admin"
-                        val password = "y"
-                    })
-                    val registerBank = mapper.writeValueAsString(object {
-                        val username = "bank"
-                        val password = "y"
-                    })
-                    for (b in mutableListOf<String>(registerAdmin, 
registerBank)) {
-                        val r = 
client.post("/demobanks/default/access-api/testing/register") {
-                            setBody(b)
-                            contentType(ContentType.Application.Json)
-                            expectSuccess = false
-                        }
-                        assert(r.status.value == 
HttpStatusCode.Forbidden.value)
-                    }
-                    // Set arbitrary balance to the bank.
-                    wireTransfer(
-                        "foo",
-                        "admin",
-                        "default",
-                        "setting the balance",
-                        "TESTKUDOS:99"
-                    )
-                    // Get admin's balance.  Not asserting; it
-                    // fails on != 200 responses.
-                    val r = 
client.get("/demobanks/default/access-api/accounts/admin") {
-                        expectSuccess = true
-                        basicAuth("admin", "foo")
-                    }
-                    println(r)
-                }
-            }
-        }
-    }
-
-    // Checks that the debit threshold belongs to the balance response.
-    @Test
-    fun debitInfoCheck() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                var r = 
client.get("/demobanks/default/access-api/accounts/foo") {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                // Checking that the response holds the debit threshold.
-                val mapper = ObjectMapper()
-                var respJson = mapper.readTree(r.bodyAsText())
-                var debitThreshold = respJson.get("debitThreshold").asText()
-                assert(debitThreshold == "1000")
-                r = client.get("/demobanks/default/access-api/accounts/admin") 
{
-                    expectSuccess = true
-                    basicAuth("admin", "foo")
-                }
-                respJson = mapper.readTree(r.bodyAsText())
-                debitThreshold = respJson.get("debitThreshold").asText()
-                assert(debitThreshold == "10000")
-            }
-        }
-    }
-
-    @Test
-    fun registerTest() {
-        // Test IBAN conflict detection.
-        withSandboxTestDatabase {
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    val bodyFoo = mapper.writeValueAsString(object {
-                        val username = "x"
-                        val password = "y"
-                        val iban = FOO_USER_IBAN
-                    })
-                    val bodyBar = mapper.writeValueAsString(object {
-                        val username = "y"
-                        val password = "y"
-                        val iban = FOO_USER_IBAN // conflicts
-                    })
-                    val bodyBaz = mapper.writeValueAsString(object {
-                        val username = "y"
-                        val password = "y"
-                        val iban = BAR_USER_IBAN
-                    })
-                    // Succeeds.
-                    
client.post("/demobanks/default/access-api/testing/register") {
-                        setBody(bodyFoo)
-                        contentType(ContentType.Application.Json)
-                        expectSuccess = true
-                    }
-                    // Hits conflict, because of the same IBAN.
-                    val r = 
client.post("/demobanks/default/access-api/testing/register") {
-                        setBody(bodyBar)
-                        expectSuccess = false
-                        contentType(ContentType.Application.Json)
-                    }
-                    assert(r.status.value == HttpStatusCode.Conflict.value)
-                    // Succeeds, because of a new IBAN.
-                    
client.post("/demobanks/default/access-api/testing/register") {
-                        setBody(bodyBaz)
-                        expectSuccess = true
-                        contentType(ContentType.Application.Json)
-                    }
-                }
-            }
-
-        }
-    }
-
-    /**
-     * This test checks that the bank hangs before responding with the list
-     * of transactions, in case there is none to return.  The timing checks
-     * that the server hangs for as long as the unblocking payment takes place
-     * but NOT as long as the long_poll_ms parameter would suggest.  This last
-     * check ensures that the response can only contain the artificial 
unblocking
-     * payment (that happens after a certain timeout).
-     */
-    @Test
-    fun longPolledTransactions() {
-        val unblockingTxTimer = Timer()
-        val testStartTime = System.currentTimeMillis()
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    launch {
-                        // long polls at most 50 seconds.
-                        val R = 
client.get("/demobanks/default/access-api/accounts/foo/transactions?long_poll_ms=50000")
 {
-                            expectSuccess = true
-                            basicAuth("foo", "foo")
-                        }
-                        assert(getTxs(R.bodyAsText()).size() == 1)
-                        val testEndTime = System.currentTimeMillis()
-                        val timeDiff = (testEndTime - testStartTime) / 1000L
-                        /**
-                         * Now checking that the server responded after the 
unblocking tx
-                         * took place and before the long poll timeout would 
occur.
-                         */
-                        println(timeDiff)
-                        assert(timeDiff in 4 .. 39)
-                    }
-                    unblockingTxTimer.schedule(
-                        delay = 4000L, // unblocks the server in (at least) 4 
seconds.
-                        action = {
-                            wireTransfer(
-                                "admin",
-                                "foo",
-                                "default",
-                                "#9",
-                                "TESTKUDOS:2"
-                            )
-                        }
-                    )
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/SandboxBankAccountTest.kt 
b/nexus/src/test/kotlin/SandboxBankAccountTest.kt
deleted file mode 100644
index 350ff3da..00000000
--- a/nexus/src/test/kotlin/SandboxBankAccountTest.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-import io.ktor.http.*
-import org.junit.Test
-import tech.libeufin.sandbox.SandboxError
-import tech.libeufin.sandbox.getBalance
-import tech.libeufin.sandbox.sandboxApp
-import tech.libeufin.sandbox.wireTransfer
-import tech.libeufin.util.buildBasicAuthLine
-import tech.libeufin.util.parseDecimal
-import tech.libeufin.util.roundToTwoDigits
-
-class SandboxBankAccountTest {
-    // Check if the balance shows debit.
-    @Test
-    fun debitBalance() {
-        withTestDatabase {
-            prepSandboxDb()
-            wireTransfer(
-                "admin",
-                "foo",
-                "default",
-                "Show up in logging!",
-                "TESTKUDOS:1"
-            )
-            /**
-             * Bank gave 1 to foo, should be -1 debit now.  Because
-             * the payment is still pending (= not booked), the pending
-             * transactions must be included in the calculation.
-             */
-            var bankBalance = getBalance("admin")
-            assert(bankBalance.roundToTwoDigits() == 
parseDecimal("-1").roundToTwoDigits())
-            wireTransfer(
-                "foo",
-                "admin",
-                "default",
-                "Show up in logging!",
-                "TESTKUDOS:5"
-            )
-            bankBalance = getBalance("admin")
-            assert(bankBalance.roundToTwoDigits() == 
parseDecimal("4").roundToTwoDigits())
-            // Trigger Insufficient funds case for users.
-            try {
-                wireTransfer(
-                    "foo",
-                    "admin",
-                    "default",
-                    "Show up in logging!",
-                    "TESTKUDOS:5000"
-                )
-            } catch (e: SandboxError) {
-                // Future versions may wrap this case into a dedicated 
exception type.
-                assert(e.statusCode == HttpStatusCode.Conflict)
-            }
-            // Trigger Insufficient funds case for the bank.
-            try {
-                wireTransfer(
-                    "admin",
-                    "foo",
-                    "default",
-                    "Show up in logging!",
-                    "TESTKUDOS:5000000"
-                )
-            } catch (e: SandboxError) {
-                // Future versions may wrap this case into a dedicated 
exception type.
-                assert(e.statusCode == HttpStatusCode.Conflict)
-            }
-            // Check balance didn't change for both parties.
-            bankBalance = getBalance("admin")
-            assert(bankBalance.roundToTwoDigits() == 
parseDecimal("4").roundToTwoDigits())
-            val fooBalance = getBalance("foo")
-            assert(fooBalance.roundToTwoDigits() == 
parseDecimal("-4").roundToTwoDigits())
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt 
b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
deleted file mode 100644
index bbfffd1b..00000000
--- a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
+++ /dev/null
@@ -1,662 +0,0 @@
-import com.fasterxml.jackson.databind.ObjectMapper
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.ktor.server.testing.*
-import kotlinx.coroutines.runBlocking
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.lowerCase
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Ignore
-import org.junit.Test
-import tech.libeufin.sandbox.*
-import tech.libeufin.util.getIban
-import tech.libeufin.util.parseAmount
-import tech.libeufin.util.roundToTwoDigits
-import java.io.File
-import java.math.BigDecimal
-import java.util.*
-
-class SandboxCircuitApiTest {
-
-    /**
-     * Testing that the admin is able to conduct ordinary
-     * account operations even on non-circuit accounts.  Recall:
-     * such accounts are just those without the cash-out address.
-     */
-    @Test
-    fun opOnNonCircuitAccounts() {
-        withTestDatabase {
-            testApplication {
-                prepSandboxDb()
-                testApplication {
-                    application(sandboxApp)
-                    // Only testing that this doesn't except.
-                    client.get("/demobanks/default/circuit-api/accounts") {
-                        expectSuccess = true
-                        basicAuth("admin", "foo")
-                    }
-                    // Trying to PATCH non circuit account
-                    
client.patch("/demobanks/default/circuit-api/accounts/exchange-0") {
-                        expectSuccess = true
-                        basicAuth("admin", "foo")
-                        contentType(ContentType.Application.Json)
-                        setBody("""
-                            {"name": "Exchange 0",
-                             "contact_data": {},
-                             "cashout_address": 
"payto://iban/SANDBOXX/${getIban()}"
-                             }
-                        """.trimIndent())
-                    }
-                    // PATCH it again passing a null name and cashout-address.
-                    
client.patch("/demobanks/default/circuit-api/accounts/exchange-0") {
-                        expectSuccess = true
-                        basicAuth("admin", "foo")
-                        contentType(ContentType.Application.Json)
-                        setBody("{ \"contact_data\": {} }")
-                    }
-                    // PATCH the password.
-                    
client.patch("/demobanks/default/circuit-api/accounts/exchange-0/auth") {
-                        expectSuccess = true
-                        basicAuth("admin", "foo")
-                        contentType(ContentType.Application.Json)
-                        setBody("{ \"new_password\": \"secret\" }")
-                    }
-                    // Check that PATCHing worked.
-                    
client.get("/demobanks/default/access-api/accounts/exchange-0") {
-                        expectSuccess = true
-                        basicAuth("exchange-0", "secret")
-                        contentType(ContentType.Application.Json)
-                    }
-                    // Deleting the account.
-                    
client.delete("/demobanks/default/circuit-api/accounts/exchange-0") {
-                        expectSuccess = true
-                        basicAuth("admin", "foo")
-                    }
-                    // Checking actual deletion.
-                    val R = 
client.get("/demobanks/default/circuit-api/accounts/exchange-0") {
-                        expectSuccess = false
-                        basicAuth("admin", "foo")
-                    }
-                    assert(R.status.value == HttpStatusCode.NotFound.value)
-                }
-            }
-        }
-    }
-    // Get /config, fails if != 200.
-    @Test
-    fun config() {
-        withSandboxTestDatabase {
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    val r= client.get("/demobanks/default/circuit-api/config")
-                    println(r.bodyAsText())
-                }
-            }
-        }
-    }
-
-    // Tests the application of cash-out ratio and fee.
-    @Test
-    fun estimationTest() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                var R = client.get(
-                    
"/demobanks/default/circuit-api/cashouts/estimates?amount_debit=TESTKUDOS:2"
-                ) {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                val mapper = ObjectMapper()
-                var respJson = mapper.readTree(R.bodyAsText())
-                val creditAmount = respJson.get("amount_credit").asText()
-                // sell ratio and fee are the following constants: 0.95 and 0.
-                // expected credit amount = 2 * 0.95 - 0 = 1.90
-                assert("CHF:1.90" == creditAmount || "CHF:1.9" == creditAmount)
-                R = client.get(
-                    
"/demobanks/default/circuit-api/cashouts/estimates?amount_credit=CHF:1.9"
-                ) {
-                    expectSuccess = true
-                    basicAuth("foo", "foo")
-                }
-                respJson = mapper.readTree(R.bodyAsText())
-                val debitAmount = respJson.get("amount_debit").asText()
-                assertWithPrint(
-                    "TESTKUDOS:2.00" == debitAmount,
-                    "'debit_amount' was $debitAmount for a 'credit_amount' of 
CHF:1.9"
-                )
-                R = client.get(
-                    
"/demobanks/default/circuit-api/cashouts/estimates?amount_credit=CHF:1&amount_debit=TESTKUDOS=1"
-                ) {
-                    expectSuccess = false
-                    basicAuth("foo", "foo")
-                }
-                assertWithPrint(
-                    R.status.value == HttpStatusCode.BadRequest.value,
-                    "Expected status code was 400, but got '${R.status.value}' 
instead."
-                )
-            }
-        }
-    }
-
-    /**
-     * Checking that the ordinary user foo doesn't get to access bar's
-     * data, but admin does.
-     */
-    @Test
-    fun accessAccountsTest() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                var R = 
client.get("/demobanks/default/circuit-api/accounts/bar") {
-                    basicAuth("foo", "foo")
-                    expectSuccess = false
-                }
-                assert(R.status.value == HttpStatusCode.Forbidden.value)
-                client.get("/demobanks/default/circuit-api/accounts/bar") {
-                    basicAuth("admin", "foo")
-                    expectSuccess = true
-                }
-            }
-        }
-    }
-    // Only tests that the calls get a 2xx status code.
-    @Test
-    fun listAccountsTest() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                var R = client.get("/demobanks/default/circuit-api/accounts") {
-                    basicAuth("admin", "foo")
-                }
-                println(R.bodyAsText())
-                client.get("/demobanks/default/circuit-api/accounts/baz") {
-                    basicAuth("admin", "foo")
-                }
-            }
-        }
-    }
-    @Test
-    fun badUuidTest() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                val R = 
client.post("/demobanks/default/circuit-api/cashouts/---invalid_UUID---/confirm")
 {
-                    expectSuccess = false
-                    basicAuth("foo", "foo")
-                    contentType(ContentType.Application.Json)
-                    setBody("{\"tan\":\"foo\"}")
-                }
-                assert(R.status.value == HttpStatusCode.BadRequest.value)
-            }
-        }
-    }
-    @Test
-    fun contactDataValidation() {
-        // Phone number.
-        assert(checkPhoneNumber("+987"))
-        assert(!checkPhoneNumber("987"))
-        assert(!checkPhoneNumber("foo"))
-        assert(!checkPhoneNumber(""))
-        assert(!checkPhoneNumber("+00"))
-        assert(checkPhoneNumber("+4900"))
-        // E-mail address
-        assert(checkEmailAddress("test@example.com"))
-        assert(!checkEmailAddress("foo.bar"))
-        assert(checkEmailAddress("foo.bar@example.com"))
-        assert(!checkEmailAddress("foo+bar@example.com"))
-        assert(checkEmailAddress("admin@example.info"))
-        assert(checkEmailAddress("AdMiN@COM.example.INFO"))
-    }
-
-    @Test
-    fun listCashouts() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                var R = client.get("/demobanks/default/circuit-api/cashouts") {
-                    expectSuccess = true
-                    basicAuth("admin", "foo")
-                }
-                assert(R.status.value == HttpStatusCode.NoContent.value)
-                transaction {
-                    CashoutOperationEntity.new {
-                        tan = "unused"
-                        uuid = UUID.randomUUID()
-                        amountDebit = "unused"
-                        amountCredit = "unused"
-                        subject = "unused"
-                        creationTime = 0L
-                        tanChannel = SupportedTanChannels.FILE // change type 
to enum?
-                        account = "foo"
-                        status = CashoutOperationStatus.PENDING
-                        cashoutAddress = "not used"
-                        buyAtRatio = "1"
-                        buyInFee = "1"
-                        sellAtRatio = "1"
-                        sellOutFee = "1"
-                    }
-                }
-                R = client.get("/demobanks/default/circuit-api/cashouts") {
-                    expectSuccess = true
-                    basicAuth("admin", "foo")
-                }
-                assert(R.status.value == HttpStatusCode.OK.value)
-                // Extract the UUID and check it.
-                val mapper = ObjectMapper()
-                var respJson = mapper.readTree(R.bodyAsText())
-                val uuid = respJson.get("cashouts").get(0).asText()
-                R = 
client.get("/demobanks/default/circuit-api/cashouts/$uuid") {
-                    expectSuccess = true
-                    basicAuth("admin", "foo")
-                }
-                assert(R.status.value == HttpStatusCode.OK.value)
-                respJson = mapper.readTree(R.bodyAsText())
-                val status = respJson.get("status").asText()
-                assert(status.uppercase() == "PENDING")
-                println(R.bodyAsText())
-                // Check that bar doesn't get foo's cash-out
-                R = 
client.get("/demobanks/default/circuit-api/cashouts?account=foo") {
-                    expectSuccess = false
-                    basicAuth("bar", "bar")
-                }
-                assert(R.status.value == HttpStatusCode.Forbidden.value)
-                // Check that foo can get its own
-                R = 
client.get("/demobanks/default/circuit-api/cashouts?account=foo") {
-                    expectSuccess = false
-                    basicAuth("foo", "foo")
-                }
-                assert(R.status.value == HttpStatusCode.OK.value)
-            }
-        }
-    }
-
-    // Testing that only the admin can change an account legal name.
-    @Test
-    fun patchPerm() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                val R 
=client.patch("/demobanks/default/circuit-api/accounts/foo") {
-                    contentType(ContentType.Application.Json)
-                    basicAuth("foo", "foo")
-                    expectSuccess = false
-                    setBody("""
-                        {
-                          "name": "new name",
-                          "contact_data": {},
-                          "cashout_address": "payto://iban/OUTSIDE"
-                        }
-                    """.trimIndent())
-                }
-                assert(R.status.value == HttpStatusCode.Forbidden.value)
-                client.patch("/demobanks/default/circuit-api/accounts/foo") {
-                    contentType(ContentType.Application.Json)
-                    basicAuth("admin", "foo")
-                    expectSuccess = true
-                    setBody("""
-                        {
-                          "name": "new name",
-                          "contact_data": {},
-                          "cashout_address": "payto://iban/OUTSIDE"
-                        }
-                    """.trimIndent())
-                }
-            }
-        }
-    }
-    // Tests the creation and confirmation of a cash-out operation.
-    @Test
-    fun cashout() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                // Register a new account.
-                client.post("/demobanks/default/circuit-api/accounts") {
-                    expectSuccess = true
-                    contentType(ContentType.Application.Json)
-                    basicAuth("admin", "foo")
-                    setBody("""
-                            {"username":"shop",
-                             "password": "secret",
-                             "contact_data": {},
-                             "name": "Test",
-                             "cashout_address": "payto://iban/SAMPLE"          
         
-                             }
-                        """.trimIndent())
-                }
-                // Give initial balance to the new account.
-                // Forcing different debt limit:
-                transaction {
-                    val configRaw = DemobankConfigPairEntity.find {
-                        DemobankConfigPairsTable.demobankName eq "default" and(
-                                DemobankConfigPairsTable.configKey eq 
"usersDebtLimit"
-                                )
-                    }.first()
-                    configRaw.configValue = 0.toString()
-                }
-                val initialBalance = "TESTKUDOS:50.00"
-                val balanceAfterCashout = "TESTKUDOS:30.00"
-                wireTransfer(
-                    debitAccount = "admin",
-                    creditAccount = "shop",
-                    subject = "cash-out",
-                    amount = initialBalance
-                )
-                // Check the balance before cashing out.
-                var R = 
client.get("/demobanks/default/access-api/accounts/shop") {
-                    basicAuth("shop", "secret")
-                }
-                val mapper = ObjectMapper()
-                var respJson = mapper.readTree(R.bodyAsText())
-                assert(respJson.get("balance").get("amount").asText() == 
initialBalance)
-                // Configure the user phone number, before the cash-out.
-                R = 
client.patch("/demobanks/default/circuit-api/accounts/shop") {
-                    contentType(ContentType.Application.Json)
-                    basicAuth("shop", "secret")
-                    setBody("""
-                        {
-                          "contact_data": {
-                            "phone": "+98765"
-                          },
-                          "cashout_address": "payto://iban/SAMPLE"
-                        }
-                    """.trimIndent())
-                }
-                assert(R.status.value == HttpStatusCode.NoContent.value)
-                /**
-                 * Cash-out a portion.  Ordering a cash-out of 20 TESTKUDOS
-                 * should result in the following final amount, that the user
-                 * will see as incoming in the fiat bank account: 19 = 20 * 
0.95 - 0.00.
-                 * Note: ratios and fees are currently hard-coded.
-                 */
-                R = client.post("/demobanks/default/circuit-api/cashouts") {
-                    contentType(ContentType.Application.Json)
-                    basicAuth("shop", "secret")
-                    setBody("""{
-                        "amount_debit": "TESTKUDOS:20",
-                        "amount_credit": "CHF:19",
-                        "tan_channel": "file"
-                    }""".trimIndent())
-                }
-                assert(R.status.value == HttpStatusCode.Accepted.value)
-                val operationUuid = 
mapper.readTree(R.readBytes()).get("uuid").asText()
-                // Check that the operation is found by the bank.
-                R = 
client.get("/demobanks/default/circuit-api/cashouts/${operationUuid}") {
-                    // Asking as the Admin but for the 'shop' account.
-                    basicAuth("admin", "foo")
-                }
-                // Check that the status is pending.
-                assert(mapper.readTree(R.readBytes()).get("status").asText() 
== "PENDING")
-                // Now confirm the operation.
-                
client.post("/demobanks/default/circuit-api/cashouts/${operationUuid}/confirm") 
{
-                    basicAuth("shop", "secret")
-                    contentType(ContentType.Application.Json)
-                    setBody("{\"tan\":\"foo\"}")
-                    expectSuccess = true
-                }
-                // Check that the operation is found by the bank and set to 
'confirmed'.
-                R = 
client.get("/demobanks/default/circuit-api/cashouts/${operationUuid}") {
-                    // Asking as the Admin but for the 'shop' account.
-                    basicAuth("foo", "foo")
-                }
-                assert(mapper.readTree(R.readBytes()).get("status").asText() 
== "CONFIRMED")
-                // Check that the amount got deducted by the account.
-                R = client.get("/demobanks/default/access-api/accounts/shop") {
-                    basicAuth("shop", "secret")
-                }
-                respJson = mapper.readTree(R.bodyAsText())
-                assert(respJson.get("balance").get("amount").asText() == 
balanceAfterCashout)
-                // Attempt to cash-out with wrong regional currency.
-                R = client.post("/demobanks/default/circuit-api/cashouts") {
-                    contentType(ContentType.Application.Json)
-                    basicAuth("shop", "secret")
-                    setBody("""{
-                        "amount_debit": "NOTFOUND:20",
-                        "amount_credit": "CHF:19",
-                        "tan_channel": "file"
-                    }""".trimIndent())
-                    expectSuccess = false
-                }
-                assert(R.status.value == HttpStatusCode.BadRequest.value)
-                // Attempt to cash-out with wrong fiat currency.
-                R = client.post("/demobanks/default/circuit-api/cashouts") {
-                    contentType(ContentType.Application.Json)
-                    basicAuth("shop", "secret")
-                    setBody("""{
-                        "amount_debit": "TESTKUDOS:20",
-                        "amount_credit": "NOTFOUND:19",
-                        "tan_channel": "file"
-                    }""".trimIndent())
-                    expectSuccess = false
-                }
-                assert(R.status.value == HttpStatusCode.BadRequest.value)
-                // Create a new cash-out and delete it.
-                R = client.post("/demobanks/default/circuit-api/cashouts") {
-                    contentType(ContentType.Application.Json)
-                    basicAuth("shop", "secret")
-                    setBody("""{
-                        "amount_debit": "TESTKUDOS:20",
-                        "amount_credit": "CHF:19",
-                        "tan_channel": "file"
-                    }""".trimIndent())
-                }
-                assert(R.status.value == HttpStatusCode.Accepted.value)
-                val toAbort = 
mapper.readTree(R.readBytes()).get("uuid").asText()
-                // Check it exists.
-                R = 
client.get("/demobanks/default/circuit-api/cashouts/${toAbort}") {
-                    // Asking as the Admin but for the 'shop' account.
-                    basicAuth("foo", "foo")
-                }
-                assert(R.status.value == HttpStatusCode.OK.value)
-                // Ask to delete the operation.
-                R = 
client.post("/demobanks/default/circuit-api/cashouts/${toAbort}/abort") {
-                    basicAuth("admin", "foo")
-                }
-                assert(R.status.value == HttpStatusCode.NoContent.value)
-                // Check actual disappearance.
-                R = 
client.get("/demobanks/default/circuit-api/cashouts/${toAbort}") {
-                    // Asking as the Admin but for the 'shop' account.
-                    basicAuth("foo", "foo")
-                }
-                assert(R.status.value == HttpStatusCode.NotFound.value)
-                // Ask to delete a confirmed operation.
-                R = 
client.post("/demobanks/default/circuit-api/cashouts/${operationUuid}/abort") {
-                    basicAuth("admin", "foo")
-                }
-                assert(R.status.value == 
HttpStatusCode.PreconditionFailed.value)
-            }
-        }
-    }
-
-    // Test user registration and deletion.
-    @Test
-    fun registration() {
-        withSandboxTestDatabase {
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    // Successful registration.
-                    var R = 
client.post("/demobanks/default/circuit-api/accounts") {
-                        expectSuccess = true
-                        contentType(ContentType.Application.Json)
-                        basicAuth("admin", "foo")
-                        setBody("""
-                            {"username":"shop",
-                             "password": "secret",
-                             "contact_data": {},
-                             "name": "Test",
-                             "cashout_address": "payto://iban/SAMPLE"          
         
-                             }
-                        """.trimIndent())
-                    }
-                    assert(R.status.value == HttpStatusCode.NoContent.value)
-                    // Check accounts list.
-                    R = client.get("/demobanks/default/circuit-api/accounts") {
-                        basicAuth("admin", "foo")
-                        expectSuccess = true
-                    }
-                    println(R.bodyAsText())
-                    // Update contact data.
-                    R = 
client.patch("/demobanks/default/circuit-api/accounts/shop") {
-                        contentType(ContentType.Application.Json)
-                        basicAuth("shop", "secret")
-                        setBody("""
-                            {"contact_data": {"email": "user@example.com"},
-                             "cashout_address": "payto://iban/SAMPLE"
-                            }
-                        """.trimIndent())
-                    }
-                    assert(R.status.value == HttpStatusCode.NoContent.value)
-                    // Get user data via the Access API.
-                    R = 
client.get("/demobanks/default/access-api/accounts/shop") {
-                        basicAuth("shop", "secret")
-                    }
-                    assert(R.status.value == HttpStatusCode.OK.value)
-                    // Get Circuit data via the Circuit API.
-                    R = 
client.get("/demobanks/default/circuit-api/accounts/shop") {
-                        basicAuth("shop", "secret")
-                    }
-                    println(R.bodyAsText())
-                    assert(R.status.value == HttpStatusCode.OK.value)
-                    // Change password.
-                    R = 
client.patch("/demobanks/default/circuit-api/accounts/shop/auth") {
-                        basicAuth("shop", "secret")
-                        setBody("{\"new_password\":\"new_secret\"}")
-                        contentType(ContentType.Application.Json)
-                    }
-                    assert(R.status.value == HttpStatusCode.NoContent.value)
-                    // Check that the password changed: expect 401 with 
previous password.
-                    R = 
client.get("/demobanks/default/access-api/accounts/shop") {
-                        basicAuth("shop", "secret")
-                    }
-                    assert(R.status.value == HttpStatusCode.Unauthorized.value)
-                    // Check that the password changed: expect 200 with 
current password.
-                    R = 
client.get("/demobanks/default/access-api/accounts/shop") {
-                        basicAuth("shop", "new_secret")
-                    }
-                    assert(R.status.value == HttpStatusCode.OK.value)
-                    // Change user balance.
-                    transaction {
-                        val account = BankAccountEntity.find {
-                            BankAccountsTable.label eq "shop"
-                        }.firstOrNull() ?: throw Exception("Circuit test 
account not found in the database!")
-                        account.bonus("TESTKUDOS:30")
-                        account
-                    }
-                    // Delete account.  Fails because the balance is not zero.
-                    R = 
client.delete("/demobanks/default/circuit-api/accounts/shop") {
-                        basicAuth("admin", "foo")
-                    }
-                    assert(R.status.value == 
HttpStatusCode.PreconditionFailed.value)
-                    // Bring the balance again to zero
-                    transaction {
-                        wireTransfer(
-                            "shop",
-                            "admin",
-                            "default",
-                            "deletion condition",
-                            "TESTKUDOS:30"
-                        )
-                    }
-                    // Now delete the account successfully.
-                    R = 
client.delete("/demobanks/default/circuit-api/accounts/shop") {
-                        basicAuth("admin", "foo")
-                    }
-                    assert(R.status.value == HttpStatusCode.NoContent.value)
-                    // Check actual deletion.
-                    R = 
client.get("/demobanks/default/access-api/accounts/shop") {
-                        basicAuth("shop", "secret")
-                    }
-                    assert(R.status.value == HttpStatusCode.NotFound.value)
-                }
-            }
-        }
-    }
-
-    // Tests the database RegEx filter on customer names.
-    @Ignore // Since no assert takes place.
-    @Test
-    fun customerFilter() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                val R = 
client.get("/demobanks/default/circuit-api/accounts?filter=b") {
-                    basicAuth("admin", "foo")
-                    expectSuccess = true
-                }
-                println(R.bodyAsText())
-            }
-        }
-    }
-
-    /**
-     * Testing that deleting a user doesn't cause a _different_ user
-     * to lose data.
-     */
-    @Test
-    fun deletionIsolation() {
-        withTestDatabase {
-            prepSandboxDb()
-            transaction {
-                // Admin makes sure foo has balance 100.
-                wireTransfer(
-                    "admin",
-                    "foo",
-                    subject = "set to 100",
-                    amount = "TESTKUDOS:100"
-                )
-                val fooBalance = getBalance("foo")
-                assert(fooBalance.roundToTwoDigits() == 
BigDecimal("100").roundToTwoDigits())
-                // Foo pays 3 to bar.
-                wireTransfer(
-                    "foo",
-                    "bar",
-                    subject = "donation",
-                    amount = "TESTKUDOS:3"
-                )
-                val barBalance = getBalance("bar")
-                assert(barBalance.roundToTwoDigits() == 
BigDecimal("3").roundToTwoDigits())
-                // Deleting foo from the system.
-                transaction {
-                    val uBankAccount = getBankAccountFromLabel("foo")
-                    val uCustomerProfile = getCustomer("foo")
-                    uBankAccount.delete()
-                    uCustomerProfile.delete()
-                }
-                val barBalanceUpdate = getBalance("bar")
-                assert(barBalanceUpdate.roundToTwoDigits() == 
BigDecimal("3").roundToTwoDigits())
-            }
-        }
-    }
-
-    @Test
-    fun tanCommandTest() {
-        /**
-         * 'tee' allows to test the SMS/e-mail command execution
-         * because it relates to STDIN and the first command line argument
-         * in the same way the SMS/e-mail command is expected to.
-         */
-        val tanLocation = File("/tmp/libeufin-tan-cmd-test.txt")
-        val tanContent = "libeufin"
-        if (tanLocation.exists()) tanLocation.delete()
-        runTanCommand(
-            command = "tee",
-            address = tanLocation.path,
-            message = tanContent
-        )
-        val maybeTan = tanLocation.readText()
-        assert(maybeTan == tanContent)
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/SandboxLegacyApiTest.kt 
b/nexus/src/test/kotlin/SandboxLegacyApiTest.kt
deleted file mode 100644
index 76ee405d..00000000
--- a/nexus/src/test/kotlin/SandboxLegacyApiTest.kt
+++ /dev/null
@@ -1,192 +0,0 @@
-import com.fasterxml.jackson.databind.ObjectMapper
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.ktor.server.testing.*
-import io.ktor.util.*
-import io.ktor.utils.io.*
-import io.netty.handler.codec.http.HttpResponseStatus
-import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
-import org.junit.Test
-import tech.libeufin.sandbox.sandboxApp
-import tech.libeufin.util.buildBasicAuthLine
-import tech.libeufin.util.getIban
-import java.io.ByteArrayOutputStream
-
-class SandboxLegacyApiTest {
-    fun dbHelper (f: () -> Unit) {
-        withTestDatabase {
-            prepSandboxDb()
-            f()
-        }
-    }
-    val mapper = ObjectMapper()
-
-    // EBICS Subscribers API.
-    @Test
-    fun adminEbicsSubscribers() {
-        dbHelper {
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    /**
-                     * Create a EBICS subscriber.  That conflicts because
-                     * MakeEnv.kt created it already, but tests access control
-                     * and conflict detection.
-                     */
-                    var body = mapper.writeValueAsString(object {
-                        val hostID = "eufinSandbox"
-                        val userID = "foo"
-                        val systemID = "foo"
-                        val partnerID = "foo"
-                    })
-                    var r: HttpResponse = 
client.post("/admin/ebics/subscribers") {
-                        expectSuccess = false
-                        contentType(ContentType.Application.Json)
-                        basicAuth("admin", "foo")
-                        setBody(body)
-                    }
-                    assert(r.status.value == HttpStatusCode.Conflict.value)
-
-                    // Check that EBICS subscriber indeed exists.
-                    r = client.get("/admin/ebics/subscribers") {
-                        basicAuth("admin", "foo")
-                    }
-                    assert(r.status.value == HttpStatusCode.OK.value)
-                    val respObj = mapper.readTree(r.bodyAsText())
-                    assert("foo" == 
respObj.get("subscribers").get(0).get("userID").asText())
-
-                    // Try same operations as above, with wrong admin 
credentials
-                    r = client.get("/admin/ebics/subscribers") {
-                        expectSuccess = false
-                        basicAuth("admin", "wrong")
-                    }
-                    assert(r.status.value == HttpStatusCode.Unauthorized.value)
-                    r = client.post("/admin/ebics/subscribers") {
-                        expectSuccess = false
-                        basicAuth("admin", "wrong")
-                    }
-                    assert(r.status.value == HttpStatusCode.Unauthorized.value)
-                    // Good credentials, but insufficient rights.
-                    r = client.get("/admin/ebics/subscribers") {
-                        expectSuccess = false
-                        basicAuth("foo", "foo")
-                    }
-                    assert(r.status.value == HttpStatusCode.Forbidden.value)
-                    r = client.post("/admin/ebics/subscribers") {
-                        expectSuccess = false
-                        basicAuth("foo", "foo")
-                    }
-                    assert(r.status.value == HttpStatusCode.Forbidden.value)
-                    /**
-                     * Give a bank account to the existing subscriber.  Bank 
account
-                     * is (implicitly / hard-coded) hosted at default demobank.
-                     */
-                    // Create new subscriber.  No need to have the related 
customer.
-                    body = mapper.writeValueAsString(object {
-                        val hostID = "eufinSandbox"
-                        val userID = "baz"
-                        val partnerID = "baz"
-                        val systemID = "foo"
-                    })
-                    client.post("/admin/ebics/subscribers") {
-                        expectSuccess = true
-                        contentType(ContentType.Application.Json)
-                        basicAuth("admin", "foo")
-                        setBody(body)
-                    }
-                    // Associate new bank account to it.
-                    body = mapper.writeValueAsString(object {
-                        val subscriber = object {
-                            val userID = "baz"
-                            val partnerID = "baz"
-                            val systemID = "baz"
-                            val hostID = "eufinSandbox"
-                        }
-                        val iban = getIban()
-                        val bic = "SANDBOXX"
-                        val name = "Now Have Account"
-                        val label = "baz"
-                    })
-                    client.post("/admin/ebics/bank-accounts") {
-                        expectSuccess = true
-                        contentType(ContentType.Application.Json)
-                        basicAuth("admin", "foo")
-                        setBody(body)
-                    }
-                    r = client.get("/admin/ebics/subscribers") {
-                        basicAuth("admin", "foo")
-                    }
-                    assert(r.status.value == HttpStatusCode.OK.value)
-                    val respObj_ = mapper.readTree(r.bodyAsText())
-                    val bankAccountLabel = 
respObj_.get("subscribers").get(1).get("demobankAccountLabel").asText()
-                    assert("baz" == bankAccountLabel)
-                    // Same operation, wrong/unauth credentials.
-                    r = client.post("/admin/ebics/bank-accounts") {
-                        expectSuccess = false
-                        basicAuth("admin", "wrong")
-                    }
-                    assert(r.status.value == HttpStatusCode.Unauthorized.value)
-                    r = client.post("/admin/ebics/bank-accounts") {
-                        expectSuccess = false
-                        basicAuth("foo", "foo")
-                    }
-                    assert(r.status.value == HttpStatusCode.Forbidden.value)
-                }
-            }
-        }
-    }
-
-    // EBICS Hosts API.
-    @Test
-    fun adminEbicsCreateHost() {
-        dbHelper {
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    val body = mapper.writeValueAsString(
-                        object {
-                            val hostID = "www"
-                            var ebicsVersion = "www"
-                        }
-                    )
-                    // Valid request, good credentials.
-                    client.post("/admin/ebics/hosts") {
-                        expectSuccess = true
-                        setBody(body)
-                        contentType(ContentType.Application.Json)
-                        basicAuth("admin", "foo")
-                    }
-                    var r = client.get("/admin/ebics/hosts") { expectSuccess = 
false }
-                    assert(r.status.value == 
HttpResponseStatus.UNAUTHORIZED.code())
-                    client.get("/admin/ebics/hosts") {
-                        basicAuth("admin", "foo")
-                        expectSuccess = true
-                    }
-                    // Invalid, with good credentials.
-                    r = client.post("/admin/ebics/hosts") {
-                        expectSuccess = false
-                        setBody("invalid")
-                        contentType(ContentType.Application.Json)
-                        basicAuth("admin", "foo")
-                    }
-                    assert(r.status.value == HttpStatusCode.BadRequest.value)
-                    // Unauth: admin with wrong password.
-                    r = client.post("/admin/ebics/hosts") {
-                        expectSuccess = false
-                        basicAuth("admin", "bar")
-                    }
-                    assert(r.status.value == HttpStatusCode.Unauthorized.value)
-                    // Auth & forbidden resource.
-                    r = client.post("/admin/ebics/hosts") {
-                        expectSuccess = false
-                        basicAuth("foo", "foo")
-                    }
-                    assert(r.status.value == HttpStatusCode.Forbidden.value)
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/SchedulingTest.kt 
b/nexus/src/test/kotlin/SchedulingTest.kt
deleted file mode 100644
index 0e1b4be9..00000000
--- a/nexus/src/test/kotlin/SchedulingTest.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-import io.ktor.client.*
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.http.*
-import io.ktor.server.testing.*
-import kotlinx.coroutines.*
-import org.junit.Ignore
-import org.junit.Test
-import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
-import tech.libeufin.nexus.server.FetchLevel
-import tech.libeufin.nexus.server.FetchSpecAllJson
-import tech.libeufin.nexus.whileTrueOperationScheduler
-import tech.libeufin.sandbox.sandboxApp
-import java.util.*
-import kotlin.concurrent.schedule
-import kotlin.text.get
-
-/**
- * This test suite helps to _measure_ the scheduler performance.
- * It is NOT meant to assert on values, but rather to _launch_ and
- * give the chance to monitor the CPU usage with TOP(1)
- */
-
-/**
- * It emerged that whether asking transactions via EBICS or x-libeufin-bank
- * is NOT performance relevant!  For example, asking for a bank account
- * balance - via the plain Access API - brings the CPU usage to > 10%.  Asking
- * for /config - via Integration API - used to oscillate the CPU usage
- * between 3 and 10%.
- *
- * The scheduler's loop style is not relevant either: a while-true & 
delay(1000)
- * or a Java Timer did NOT change the perf.
- */
-
-// This class focuses on the perf. of Nexus scheduling.
-class SchedulingTest {
-    // Launching the scheduler to measure its perf with TOP(1)
-    @Ignore // Ignoring because no assert takes place.
-    @Test
-    fun normalOperation() {
-        withTestDatabase {
-            prepNexusDb()
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                whileTrueOperationScheduler(client)
-                // javaTimerOperationScheduler(client)
-            }
-        }
-        runBlocking {
-            launch { awaitCancellation() }
-        }
-    }
-
-    // Allows TOP(1) on the bare connection operations without the scheduling 
overhead.
-    // Not strictly related to scheduling, but perf. is a major part of 
scheduling.
-    @Test
-    @Ignore // Ignoring because no assert takes place.
-    fun bareOperationXLibeufinBank() {
-        withTestDatabase {
-            prepNexusDb()
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    while (true) {
-                        // Even x-libeufin-bank takes 10-20% CPU
-                        fetchBankAccountTransactions(
-                            client,
-                            fetchSpec = FetchSpecAllJson(
-                                level = FetchLevel.STATEMENT,
-                                bankConnection = "bar"
-                            ),
-                            accountId = "bar"
-                        )
-                        delay(1000L)
-                    }
-                }
-            }
-        }
-    }
-    // Same as the previous, but on a EBICS connection.
-    // Perf. is only slightly worse than the JSON based x-libeufin-bank 
connection.
-    @Ignore // Ignoring because no assert takes place.
-    @Test
-    fun bareOperationEbics() {
-        withTestDatabase {
-            prepNexusDb()
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                runBlocking {
-                    while (true) {
-                        fetchBankAccountTransactions(
-                            client,
-                            fetchSpec = FetchSpecAllJson(
-                                level = FetchLevel.STATEMENT,
-                                bankConnection = "foo"
-                            ),
-                            accountId = "foo"
-                        )
-                        delay(1000L)
-                    }
-                }
-            }
-        }
-    }
-
-    // HTTP requests loop, to measure perf. via TOP(1)
-    @Ignore // because no assert takes place.
-    @Test
-    fun plainSandboxReqLoop() {
-        withTestDatabase {
-            prepSandboxDb()
-            testApplication {
-                application(sandboxApp)
-                while (true) {
-                    // This brings the CPU to > 10%
-                    /*client.get("/demobanks/default/access-api/accounts/foo") 
{
-                        expectSuccess = true
-                        contentType(ContentType.Application.Json)
-                        basicAuth("foo", "foo")
-                    }*/
-                    // This brings the CPU between 3 and 10%
-                    /*client.get("/demobanks/default/integration-api/config") {
-                        expectSuccess = true
-                        contentType(ContentType.Application.Json)
-                        // This caused 3 to 9% CPU => did not cause more usage.
-                        // basicAuth("foo", "foo")
-                    }*/
-                    // Between 2 and 3% CPU.
-                    client.get("/")
-                    delay(1000L)
-                }
-            }
-        }
-    }
-}
-
-// This class investigates two ways of scheduling, regardless of the one used 
by Nexus.
-class PlainJavaScheduling {
-    val instanceTimer = Timer()
-    // Below 5% CPU time.
-    private fun loopWithJavaTimer() {
-        println("with Java Timer " +
-                "doing at ${System.currentTimeMillis() / 1000}.."
-        ) // uncertain time goes by.
-        instanceTimer.schedule(
-            delay = 1200,
-            action = { loopWithJavaTimer() }
-        )
-    }
-    // Below 5% CPU time.
-    private suspend fun loopWithWhileTrue() {
-        val client = HttpClient()
-        while (true) {
-            println("With while-true " +
-                    "doing at ${System.currentTimeMillis() / 1000}.."
-            ) // uncertain time goes by.
-            client.get("https://exchange.demo.taler.net/wrong";) {
-                basicAuth("foo", "foo")
-            }
-            delay(1000)
-        }
-    }
-    @Ignore // due to no assert.
-    @Test
-    fun javaTimerLoop() {
-        loopWithJavaTimer()
-        runBlocking { delay(timeMillis = 30000) }
-    }
-    @Ignore // due to no assert.
-    @Test
-    fun whileTrueLoop() {
-        runBlocking {
-            loopWithWhileTrue()
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/SplitString.kt 
b/nexus/src/test/kotlin/SplitString.kt
deleted file mode 100644
index a0fb0069..00000000
--- a/nexus/src/test/kotlin/SplitString.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package tech.libeufin.nexus
-
-import org.junit.Test
-
-class SplitString {
-
-    @Test
-    fun splitString() {
-        val chunks = mutableListOf<String>("first", "second", "third", 
"fourth")
-        val join = chunks.joinToString("|")
-        val chunkAgain = join.split("|")
-        assert(chunks == chunkAgain)
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/SubjectNormalization.kt 
b/nexus/src/test/kotlin/SubjectNormalization.kt
deleted file mode 100644
index d6eee674..00000000
--- a/nexus/src/test/kotlin/SubjectNormalization.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-import org.junit.Test
-import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.extractReservePubFromSubject
-
-class SubjectNormalization {
-
-    @Test
-    fun testBeforeAndAfter() {
-        val mereValue = "1ENVZ6EYGB6Z509KRJ6E59GK1EQXZF8XXNY9SN33C2KDGSHV9KA0"
-        assert(mereValue == extractReservePubFromSubject(mereValue))
-        assert(mereValue == extractReservePubFromSubject("noise before 
${mereValue} noise after"))
-        val mereValueNewLines = 
"\t1ENVZ6EYGB6Z\n\n\n509KRJ6E59GK1EQXZF8XXNY9\nSN33C2KDGSHV9KA0"
-        assert(mereValue == extractReservePubFromSubject(mereValueNewLines))
-        assert(mereValue == extractReservePubFromSubject("noise before 
$mereValueNewLines noise after"))
-    }
-
-    /**
-     * Here we test whether the value that the extractor picks
-     * from a payment subjects is then validated by the crypto backend.
-     */
-    @Test
-    fun extractorVsDecoder() {
-        val validPub = "7R422Z6C5TPG0JM32KRWV093J0AG0GVZV1247F9PBSFZT6Y61G1G"
-        assert(CryptoUtil.checkValidEddsaPublicKey(validPub))
-        // Swapping zeros with Os.
-        assert(CryptoUtil.checkValidEddsaPublicKey(validPub.replace('0', 'O')))
-        // At this point, the decoder handles 0s and Os interchangeably.
-        // Now check that the reserve pub. extractor behaves equally.
-        val extractedPub = extractReservePubFromSubject(validPub) // has 0s.
-        // The "!!" ensures that the extractor found a likely reserve pub.
-        assert(CryptoUtil.checkValidEddsaPublicKey(extractedPub!!))
-        val extractedPubWithOs = 
extractReservePubFromSubject(validPub.replace('0', 'O'))
-        // The "!!" ensures that the extractor did find the reserve pub. with 
Os instead of zeros.
-        assert(CryptoUtil.checkValidEddsaPublicKey(extractedPubWithOs!!))
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/TalerTest.kt 
b/nexus/src/test/kotlin/TalerTest.kt
deleted file mode 100644
index 8c9a5edd..00000000
--- a/nexus/src/test/kotlin/TalerTest.kt
+++ /dev/null
@@ -1,260 +0,0 @@
-import com.fasterxml.jackson.databind.ObjectMapper
-import io.ktor.client.call.*
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.ktor.server.testing.*
-import kotlinx.coroutines.*
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Ignore
-import org.junit.Test
-import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
-import tech.libeufin.nexus.bankaccount.submitAllPaymentInitiations
-import tech.libeufin.nexus.ingestFacadeTransactions
-import tech.libeufin.nexus.maybeTalerRefunds
-import tech.libeufin.nexus.server.*
-import tech.libeufin.nexus.talerFilter
-import tech.libeufin.sandbox.sandboxApp
-import tech.libeufin.sandbox.wireTransfer
-import tech.libeufin.util.NotificationsChannelDomains
-import tech.libeufin.util.getIban
-
-// This class tests the features related to the Taler facade.
-class TalerTest {
-    private val mapper = ObjectMapper()
-
-    @Test
-    fun historyOutgoingTestEbics() {
-        historyOutgoingTest("foo")
-    }
-    @Test
-    fun historyOutgoingTestXLibeufinBank() {
-        historyOutgoingTest("bar")
-    }
-
-    // Checking that a call to POST /transfer results in
-    // an outgoing payment in GET /history/outgoing.
-    fun historyOutgoingTest(testedAccount: String) {
-        withNexusAndSandboxUser {
-            testApplication {
-                application(nexusApp)
-                
client.post("/facades/$testedAccount-facade/taler-wire-gateway/transfer") {
-                    contentType(ContentType.Application.Json)
-                    basicAuth(testedAccount, testedAccount) // exchange's 
credentials
-                    expectSuccess = true
-                    setBody("""
-                        { "request_uid": "twg_transfer_0",
-                          "amount": "TESTKUDOS:3",
-                          "exchange_base_url": "http://exchange.example.com/";,
-                          "wtid": "T0",
-                          "credit_account": 
"payto://iban/${BANK_IBAN}?receiver-name=Not-Used"
-                        }
-                    """.trimIndent())
-                }
-            }
-            /* The bank connection sends the payment instruction to the bank 
here.
-             * and the reconciliation mechanism in Nexus should detect that one
-             * outgoing payment was indeed the one instructed via the TWG.  The
-             * reconciliation will make the outgoing payment visible via 
/history/outgoing.
-             * The following block achieve this by starting Sandbox and 
sending all
-             * the prepared payments to it.
-             */
-            testApplication {
-                application(sandboxApp)
-                submitAllPaymentInitiations(client, testedAccount)
-                /* Now downloads transactions from the bank, where the payment
-                   submitted in the previous block is expected to appear as 
outgoing.
-                 */
-                fetchBankAccountTransactions(
-                    client,
-                    fetchSpec = FetchSpecTimeRangeJson(
-                        level = if (testedAccount == "bar") 
FetchLevel.STATEMENT else FetchLevel.REPORT,
-                        start = "2020-01-01",
-                        end = "3000-01-01",
-                        bankConnection = testedAccount
-                    ),
-                    accountId = testedAccount
-                )
-            }
-            /**
-             * Now Nexus starts again, in order to serve /history/outgoing
-             * along the TWG.
-             */
-            testApplication {
-                application(nexusApp)
-                val r = 
client.get("/facades/$testedAccount-facade/taler-wire-gateway/history/outgoing?delta=5")
 {
-                    expectSuccess = true
-                    contentType(ContentType.Application.Json)
-                    basicAuth(testedAccount, testedAccount)
-                }
-                assert(r.status.value == HttpStatusCode.OK.value)
-                val j = mapper.readTree(r.readBytes())
-                val wtidFromTwg = 
j.get("outgoing_transactions").get(0).get("wtid").asText()
-                assert(wtidFromTwg == "T0")
-            }
-        }
-    }
-
-    // Tests that incoming Taler txs arrive via EBICS.
-    @Test
-    fun historyIncomingTestEbics() {
-        historyIncomingTest(
-            testedAccount = "foo",
-            connType = BankConnectionType.EBICS
-        )
-    }
-
-    // Tests that incoming Taler txs arrive via x-libeufin-bank.
-    @Test
-    fun historyIncomingTestXLibeufinBank() {
-        historyIncomingTest(
-            testedAccount = "bar",
-            connType = BankConnectionType.X_LIBEUFIN_BANK
-        )
-    }
-
-    // Tests that even if one call is long-polling, other calls respond.
-    @Test
-    fun servingTest() {
-        withTestDatabase {
-            prepNexusDb()
-            testApplication {
-                application(nexusApp)
-                val currentTime = System.currentTimeMillis()
-                runBlocking {
-                    launch {
-                        val r = 
client.get("/facades/foo-facade/taler-wire-gateway/history/incoming?delta=5&start=0&long_poll_ms=5000")
 {
-                            expectSuccess = true
-                            contentType(ContentType.Application.Json)
-                            basicAuth("foo", "foo") // user & pw always equal.
-                        }
-                        assert(r.status.value == 
HttpStatusCode.NoContent.value)
-                    }
-                    val R = client.get("/") {
-                        expectSuccess = true
-                    }
-                    val latestTime = System.currentTimeMillis()
-                    // Checks that the call didn't hang for the whole 
long_poll_ms.
-                    assert(R.status.value == HttpStatusCode.OK.value
-                            && (latestTime - currentTime) < 2000
-                    )
-                }
-            }
-        }
-    }
-
-    // Downloads Taler txs via the default connection of 'testedAccount'.
-    // This allows to test the Taler logic on different connection types.
-    private fun historyIncomingTest(testedAccount: String, connType: 
BankConnectionType) {
-        val reservePub = "GX5H5RME193FDRCM1HZKERXXQ2K21KH7788CKQM8X6MYKYRBP8F0"
-        withNexusAndSandboxUser {
-            testApplication {
-                application(nexusApp)
-                runBlocking {
-                    /**
-                     * This block issues the request by long-polling and
-                     * lets the execution proceed where the actions to unblock
-                     * the polling are taken.
-                     */
-                    launch {
-                        val r = 
client.get("/facades/${testedAccount}-facade/taler-wire-gateway/history/incoming?delta=5&start=0&long_poll_ms=30000")
 {
-                            expectSuccess = true
-                            contentType(ContentType.Application.Json)
-                            basicAuth(testedAccount, testedAccount) // user & 
pw always equal.
-                        }
-                        assertWithPrint(
-                            r.status.value == HttpStatusCode.OK.value,
-                            "Long-polling history had status: 
${r.status.value} and" +
-                                    " body: ${r.bodyAsText()}"
-                        )
-                        val j = mapper.readTree(r.readBytes())
-                        val reservePubFromTwg = 
j.get("incoming_transactions").get(0).get("reserve_pub").asText()
-                        assert(reservePubFromTwg == reservePub)
-                    }
-                    launch {
-                        delay(500)
-                        newNexusBankTransaction(
-                            currency = "KUDOS",
-                            value = "10",
-                            subject = reservePub,
-                            creditorAcct = testedAccount,
-                            connType = connType
-                        )
-                        ingestFacadeTransactions(
-                            bankAccountId = testedAccount, // bank account 
local to Nexus.
-                            facadeType = NexusFacadeType.TALER,
-                            incomingFilterCb = ::talerFilter,
-                            refundCb = ::maybeTalerRefunds
-                        )
-                    }
-                }
-            }
-        }
-    }
-
-    @Ignore // Ignoring because no assert takes place.
-    @Test // Triggering a refund because of a duplicate reserve pub.
-    fun refundTest() {
-        withNexusAndSandboxUser {
-            // Creating a Taler facade for the user 'foo'.
-            testApplication {
-                application(nexusApp)
-                client.post("/facades") {
-                    expectSuccess = true
-                    contentType(ContentType.Application.Json)
-                    basicAuth("foo", "foo")
-                    setBody("""
-                        { "name":"foo-facade",
-                          "type":"taler-wire-gateway",
-                          "config": {
-                            "bankAccount":"foo",
-                            "bankConnection":"foo",
-                            "currency":"TESTKUDOS",
-                            "reserveTransferLevel":"report"
-                          }
-                    }""".trimIndent()
-                    )
-                }
-            }
-            wireTransfer(
-                "bar",
-                "foo",
-                demobank = "default",
-                "5WFM8PXN7Y315RVZFJ280299B94W1HR1AAHH6XNDYEJBC0T3E5N0",
-                "TESTKUDOS:3"
-            )
-            testApplication {
-                application(sandboxApp)
-                // Nexus downloads the fresh transaction.
-                fetchBankAccountTransactions(
-                    client,
-                    fetchSpec = FetchSpecAllJson(
-                        level = FetchLevel.REPORT,
-                        "foo"
-                    ),
-                    "foo"
-                )
-            }
-            wireTransfer(
-                "bar",
-                "foo",
-                demobank = "default",
-                "5WFM8PXN7Y315RVZFJ280299B94W1HR1AAHH6XNDYEJBC0T3E5N0",
-                "TESTKUDOS:3"
-            )
-            testApplication {
-                application(sandboxApp)
-                // Nexus downloads the new transaction, having a duplicate 
subject.
-                fetchBankAccountTransactions(
-                    client,
-                    fetchSpec = FetchSpecAllJson(
-                        level = FetchLevel.REPORT,
-                        "foo"
-                    ),
-                    "foo"
-                )
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/XLibeufinBankTest.kt 
b/nexus/src/test/kotlin/XLibeufinBankTest.kt
deleted file mode 100644
index 7961f9db..00000000
--- a/nexus/src/test/kotlin/XLibeufinBankTest.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.server.testing.*
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Test
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.bankaccount.addPaymentInitiation
-import tech.libeufin.nexus.bankaccount.ingestBankMessagesIntoAccount
-import tech.libeufin.nexus.server.*
-import tech.libeufin.nexus.xlibeufinbank.XlibeufinBankConnectionProtocol
-import tech.libeufin.sandbox.BankAccountTransactionEntity
-import tech.libeufin.sandbox.BankAccountTransactionsTable
-import tech.libeufin.sandbox.sandboxApp
-import tech.libeufin.sandbox.wireTransfer
-import tech.libeufin.util.XLibeufinBankTransaction
-import tech.libeufin.util.getIban
-import java.net.URL
-
-// Testing the x-libeufin-bank communication
-
-class XLibeufinBankTest {
-    private val mapper = jacksonObjectMapper()
-    @Test
-    fun urlParse() {
-        val u = URL("http://localhost";)
-        println(u.authority)
-    }
-
-    /**
-     * This test tries to submit a transaction to Sandbox
-     * via the x-libeufin-bank connection and later - after
-     * having downloaded its transactions - tries to reconcile
-     * it as sent.
-     */
-    @Test
-    fun submitTransaction() {
-        withTestDatabase {
-            prepSandboxDb()
-            prepNexusDb()
-            testApplication {
-                application(sandboxApp)
-                val pId = addPaymentInitiation(
-                    Pain001Data(
-                        creditorIban = FOO_USER_IBAN,
-                        creditorBic = "SANDBOXX",
-                        creditorName = "Tester",
-                        subject = "test payment",
-                        sum = "1",
-                        currency = "TESTKUDOS"
-                    ),
-                    transaction {
-                        NexusBankAccountEntity.findByName("bar") ?:
-                        throw Exception("Test failed, env didn't provide Nexus 
bank account 'bar'")
-                    }
-                )
-                val conn = XlibeufinBankConnectionProtocol()
-                conn.submitPaymentInitiation(this.client, pId.id.value)
-                val maybeArrivedPayment = transaction {
-                    BankAccountTransactionEntity.find {
-                        BankAccountTransactionsTable.pmtInfId eq 
pId.paymentInformationId
-                    }.firstOrNull()
-                }
-                // Now look for the payment in the database.
-                assert(maybeArrivedPayment != null)
-            }
-        }
-    }
-    /**
-     * Testing that Nexus downloads one transaction from
-     * Sandbox via the x-libeufin-bank protocol supplier
-     * and stores it in the Nexus internal transactions
-     * table.
-     *
-     * NOTE: the test should be extended by checking that
-     * downloading twice the transaction doesn't lead to asset
-     * duplication locally in Nexus.
-     */
-    @Test
-    fun fetchTransaction() {
-        withTestDatabase {
-            prepSandboxDb()
-            prepNexusDb()
-            testApplication {
-                // Creating the Sandbox transaction that's expected to be 
ingested.
-                wireTransfer(
-                    debitAccount = "bar",
-                    creditAccount = "foo",
-                    demobank = "default",
-                    subject = "x-libeufin-bank test transaction",
-                    amount = "TESTKUDOS:333"
-                )
-                val fooUser = getNexusUser("foo")
-                // Creating the x-libeufin-bank connection to interact with 
Sandbox.
-                val conn = XlibeufinBankConnectionProtocol()
-                val jDetails = """{
-                    "username": "foo",
-                    "password": "foo",
-                    "baseUrl": "http://localhost/demobanks/default/access-api";
-                    }""".trimIndent()
-                conn.createConnection(
-                    connId = "x",
-                    user = fooUser,
-                    data = mapper.readTree(jDetails)
-                )
-                // Starting _Sandbox_ to check how it reacts to Nexus request.
-                application(sandboxApp)
-                /**
-                 * Doing two rounds of download: the first is expected to
-                 * record the payment as new, and the second is expected to
-                 * ignore it because it has already it in the database.
-                 */
-                repeat(2) {
-                    // Invoke transaction fetcher offered by the 
x-libeufin-bank connection.
-                    conn.fetchTransactions(
-                        fetchSpec = FetchSpecAllJson(
-                            FetchLevel.STATEMENT,
-                            null
-                        ),
-                        accountId = "foo",
-                        bankConnectionId = "x",
-                        client = client
-                    )
-                }
-                // The messages are in the database now, invoke the
-                // ingestion routine to parse them into the Nexus internal
-                // format.
-                ingestBankMessagesIntoAccount("x", "foo")
-                // Asserting that the payment made it to the database in the 
Nexus format.
-                transaction {
-                    val maybeTx = NexusBankTransactionEntity.all()
-                    // This assertion checks that the payment is not doubled 
in the database:
-                    assert(maybeTx.count() == 1L)
-                    val tx = 
maybeTx.first().parseDetailsIntoObject<CamtBankAccountEntry>()
-                    assert(tx.getSingletonSubject() == "x-libeufin-bank test 
transaction")
-                }
-            }
-        }
-    }
-
-    // Testing that Nexus responds with correct connection details.
-    // Currently only testing that the request doesn't throw any error.
-    @Test
-    fun connectionDetails() {
-        withTestDatabase {
-            prepNexusDb()
-            testApplication {
-                application(nexusApp)
-                val r = client.get("/bank-connections/bar") {
-                    basicAuth("bar", "bar")
-                    expectSuccess = true
-                }
-                println(r.bodyAsText())
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/XPathTest.kt 
b/nexus/src/test/kotlin/XPathTest.kt
deleted file mode 100644
index be48c4fb..00000000
--- a/nexus/src/test/kotlin/XPathTest.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package tech.libeufin.nexus
-
-import org.junit.Test
-import org.w3c.dom.Document
-import tech.libeufin.util.XMLUtil
-import tech.libeufin.util.pickString
-
-class XPathTest {
-
-    @Test
-    fun pickDataFromSimpleXml() {
-        val xml = """
-            <root xmlns="foo">
-              <node>lorem ipsum</node>
-            </root>""".trimIndent()
-        val doc: Document = XMLUtil.parseStringIntoDom(xml)
-        println(doc.pickString( "//*[local-name()='node']"))
-    }
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git 
a/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml 
b/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml
deleted file mode 100644
index 14a36eb5..00000000
--- a/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml
+++ /dev/null
@@ -1,488 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- This file has been placed in the public domain -->
-<!-- Sample camt.053 according to the interpretation of the German DK rules -->
-<!-- IBANs have been randomly generated with a BBAN of 12345678 -->
-<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02 
camt.053.001.02.xsd">
-    <BkToCstmrStmt>
-        <GrpHdr>
-            <MsgId>msg-001</MsgId>
-            <CreDtTm>2020-07-03T12:44:40+05:30</CreDtTm>
-        </GrpHdr>
-        <Stmt>
-            <Id>stmt-001</Id>
-            <CreDtTm>2020-07-03T11:00:40+05:30</CreDtTm>
-            <Acct>
-                <Id>
-                    <IBAN>DE54123456784713474163</IBAN>
-                </Id>
-            </Acct>
-            <Bal>
-                <Tp>
-                    <CdOrPrtry>
-                        <Cd>PRCD</Cd>
-                    </CdOrPrtry>
-                </Tp>
-                <Amt Ccy="EUR">500</Amt>
-                <CdtDbtInd>CRDT</CdtDbtInd>
-                <Dt>
-                    <Dt>2020-07-03</Dt>
-                </Dt>
-            </Bal>
-
-            <!-- Credit due to incoming SCT -->
-            <Ntry>
-                <Amt Ccy="EUR">100.00</Amt>
-                <CdtDbtInd>CRDT</CdtDbtInd>
-                <Sts>BOOK</Sts>
-                <BookgDt>
-                    <Dt>2020-07-02</Dt>
-                </BookgDt>
-                <ValDt>
-                    <Dt>2020-07-04</Dt>
-                </ValDt>
-                <AcctSvcrRef>acctsvcrref-001</AcctSvcrRef>
-                <BkTxCd>
-                    <Domn>
-                        <Cd>PMNT</Cd>
-                        <Fmly>
-                            <Cd>RCDT</Cd>
-                            <SubFmlyCd>ESCT</SubFmlyCd>
-                        </Fmly>
-                    </Domn>
-                    <Prtry>
-                        <Cd>166</Cd>
-                        <Issr>DK</Issr>
-                    </Prtry>
-                </BkTxCd>
-                <NtryDtls>
-                    <TxDtls>
-                        <Refs>
-                            <EndToEndId>e2e-001</EndToEndId>
-                        </Refs>
-                        <BkTxCd>
-                            <Domn>
-                                <Cd>PMNT</Cd>
-                                <Fmly>
-                                    <Cd>RCDT</Cd>
-                                    <SubFmlyCd>ESCT</SubFmlyCd>
-                                </Fmly>
-                            </Domn>
-                            <Prtry>
-                                <Cd>NTRF+166</Cd>
-                                <Issr>DK</Issr>
-                            </Prtry>
-                        </BkTxCd>
-                        <RltdPties>
-                            <Dbtr>
-                                <Nm>Debtor One</Nm>
-                            </Dbtr>
-                            <DbtrAcct>
-                                <Id>
-                                    <IBAN>DE52123456789473323175</IBAN>
-                                </Id>
-                            </DbtrAcct>
-                            <UltmtDbtr>
-                                <Nm>Ultimate Debtor One</Nm>
-                            </UltmtDbtr>
-                            <Cdtr>
-                                <Nm>Creditor One</Nm>
-                            </Cdtr>
-                            <UltmtCdtr>
-                                <Nm>Ultimate Creditor One</Nm>
-                            </UltmtCdtr>
-                        </RltdPties>
-                        <Purp>
-                            <Cd>GDDS</Cd>
-                        </Purp>
-                        <RmtInf>
-                            <Ustrd>unstructured info one</Ustrd>
-                        </RmtInf>
-                    </TxDtls>
-                </NtryDtls>
-                <AddtlNtryInf>SEPA GUTSCHRIFT</AddtlNtryInf>
-            </Ntry>
-
-            <!-- Entry to illustrate multiple ustrd elements -->
-            <Ntry>
-                <Amt Ccy="EUR">50.00</Amt>
-                <CdtDbtInd>CRDT</CdtDbtInd>
-                <Sts>BOOK</Sts>
-                <BookgDt>
-                    <Dt>2020-07-02</Dt>
-                </BookgDt>
-                <ValDt>
-                    <Dt>2020-07-04</Dt>
-                </ValDt>
-                <AcctSvcrRef>acctsvcrref-002</AcctSvcrRef>
-                <BkTxCd>
-                    <Domn>
-                        <Cd>PMNT</Cd>
-                        <Fmly>
-                            <Cd>RCDT</Cd>
-                            <SubFmlyCd>ESCT</SubFmlyCd>
-                        </Fmly>
-                    </Domn>
-                    <Prtry>
-                        <Cd>166</Cd>
-                        <Issr>DK</Issr>
-                    </Prtry>
-                </BkTxCd>
-                <!-- Credit due to incoming SCT -->
-                <NtryDtls>
-                    <TxDtls>
-                        <Refs>
-                            <EndToEndId>e2e-002</EndToEndId>
-                        </Refs>
-                        <BkTxCd>
-                            <Domn>
-                                <Cd>PMNT</Cd>
-                                <Fmly>
-                                    <Cd>RCDT</Cd>
-                                    <SubFmlyCd>ESCT</SubFmlyCd>
-                                </Fmly>
-                            </Domn>
-                            <Prtry>
-                                <Cd>NTRF+166</Cd>
-                                <Issr>DK</Issr>
-                            </Prtry>
-                        </BkTxCd>
-                        <RltdPties>
-                            <Dbtr>
-                                <Nm>Debtor One</Nm>
-                            </Dbtr>
-                            <DbtrAcct>
-                                <Id>
-                                    <IBAN>DE52123456789473323175</IBAN>
-                                </Id>
-                            </DbtrAcct>
-                            <Cdtr>
-                                <Nm>Creditor One</Nm>
-                            </Cdtr>
-                        </RltdPties>
-                        <RmtInf>
-                            <Ustrd>unstructured </Ustrd>
-                            <Ustrd>info </Ustrd>
-                            <Ustrd>across </Ustrd>
-                            <Ustrd>lines</Ustrd>
-                        </RmtInf>
-                    </TxDtls>
-                </NtryDtls>
-            </Ntry>
-
-            <!--
-            Credit due to a return resulting from a batch payment initiation 
where only one payment failed.
-            This data was obtained by doing a transaction on a GLS Bank 
account, but we've replaced
-            the account's IBAN with a random one.
-            Note how the original creditor and debtor are preserved and not 
flipped.
-            Unfortunately the original payment didn't have an end-to-end ID, 
so it would be harder
-            to correlate this message to the original payment initiation -->
-            <Ntry>
-                <Amt Ccy="EUR">1.12</Amt>
-                <CdtDbtInd>CRDT</CdtDbtInd>
-                <Sts>BOOK</Sts>
-                <BookgDt>
-                    <Dt>2020-06-30</Dt>
-                </BookgDt>
-                <ValDt>
-                    <Dt>2020-06-30</Dt>
-                </ValDt>
-                <AcctSvcrRef>2020063011423362000</AcctSvcrRef>
-                <BkTxCd>
-                    <Domn>
-                        <Cd>PMNT</Cd>
-                        <Fmly>
-                            <Cd>ICDT</Cd>
-                            <SubFmlyCd>RRTN</SubFmlyCd>
-                        </Fmly>
-                    </Domn>
-                    <Prtry>
-                        <Cd>NRTI+159+00931</Cd>
-                        <Issr>DK</Issr>
-                    </Prtry>
-                </BkTxCd>
-                <NtryDtls>
-                    <TxDtls>
-                        <Refs>
-                            <EndToEndId>NOTPROVIDED</EndToEndId>
-                        </Refs>
-                        <AmtDtls>
-                            <TxAmt>
-                                <Amt Ccy="EUR">1.12</Amt>
-                            </TxAmt>
-                        </AmtDtls>
-                        <BkTxCd>
-                            <Domn>
-                                <Cd>PMNT</Cd>
-                                <Fmly>
-                                    <Cd>ICDT</Cd>
-                                    <SubFmlyCd>RRTN</SubFmlyCd>
-                                </Fmly>
-                            </Domn>
-                            <Prtry>
-                                <Cd>NRTI+159+00931</Cd>
-                                <Issr>DK</Issr>
-                            </Prtry>
-                        </BkTxCd>
-                        <RltdPties>
-                            <Dbtr>
-                                <Nm>Account Owner</Nm>
-                            </Dbtr>
-                            <DbtrAcct>
-                                <Id>
-                                    <IBAN>DE54123456784713474163</IBAN>
-                                </Id>
-                            </DbtrAcct>
-                            <Cdtr>
-                                <Nm>Nonexistent Creditor</Nm>
-                            </Cdtr>
-                            <CdtrAcct>
-                                <Id>
-                                    <IBAN>DE24500105177398216438</IBAN>
-                                </Id>
-                            </CdtrAcct>
-                        </RltdPties>
-                        <RmtInf>
-                            <Ustrd>Retoure SEPA Ueberweisung vom 29.06.2020, 
Rueckgabegrund: AC01 IBAN fehlerhaft und ungültig SVWZ: RETURN, Sammelposten 
Nummer Zwei IBAN: DE2</Ustrd>
-                            <Ustrd>4500105177398216438 BIC: INGDDEFFXXX</Ustrd>
-                        </RmtInf>
-                        <RtrInf>
-                            <OrgnlBkTxCd>
-                                <Prtry>
-                                    <Cd>116</Cd>
-                                    <Issr>DK</Issr>
-                                </Prtry>
-                            </OrgnlBkTxCd>
-                            <Orgtr>
-                                <Id>
-                                    <OrgId>
-                                        <BICOrBEI>GENODEM1GLS</BICOrBEI>
-                                    </OrgId>
-                                </Id>
-                            </Orgtr>
-                            <Rsn>
-                                <Cd>AC01</Cd>
-                            </Rsn>
-                            <AddtlInf>IBAN fehlerhaft und ungültig</AddtlInf>
-                        </RtrInf>
-                    </TxDtls>
-                </NtryDtls>
-                <AddtlNtryInf>Retouren</AddtlNtryInf>
-            </Ntry>
-
-            <!-- Credit due to incoming USD transfer -->
-            <Ntry>
-                <Amt Ccy="EUR">1000</Amt>
-                <CdtDbtInd>CRDT</CdtDbtInd>
-                <Sts>BOOK</Sts>
-                <BookgDt>
-                    <Dt>2020-07-03</Dt>
-                </BookgDt>
-                <ValDt>
-                    <Dt>2020-07-04</Dt>
-                </ValDt>
-                <AcctSvcrRef>acctsvcrref-002</AcctSvcrRef>
-                <BkTxCd>
-                    <Domn>
-                        <Cd>PMNT</Cd>
-                        <Fmly>
-                            <Cd>RCDT</Cd>
-                            <SubFmlyCd>XBCT</SubFmlyCd>
-                        </Fmly>
-                    </Domn>
-                    <Prtry>
-                        <Cd>NTRF+202</Cd>
-                        <Issr>DK</Issr>
-                    </Prtry>
-                </BkTxCd>
-                <NtryDtls>
-                    <TxDtls>
-                        <AmtDtls>
-                            <InstdAmt>
-                                <Amt Ccy="USD">1500</Amt>
-                            </InstdAmt>
-                            <TxAmt>
-                                <Amt Ccy="EUR">1000</Amt>
-                            </TxAmt>
-                            <CntrValAmt>
-                                <Amt Ccy="EUR">1250.0</Amt>
-                                <CcyXchg>
-                                    <SrcCcy>USD</SrcCcy>
-                                    <TrgtCcy>EUR</TrgtCcy>
-                                    <XchgRate>1.20</XchgRate>
-                                </CcyXchg>
-                            </CntrValAmt>
-                        </AmtDtls>
-                        <BkTxCd>
-                            <Domn>
-                                <Cd>PMNT</Cd>
-                                <Fmly>
-                                    <Cd>RCDT</Cd>
-                                    <SubFmlyCd>XBCT</SubFmlyCd>
-                                </Fmly>
-                            </Domn>
-                            <Prtry>
-                                <Cd>NTRF+202</Cd>
-                                <Issr>DK</Issr>
-                            </Prtry>
-                        </BkTxCd>
-                        <Chrgs>
-                            <Amt Ccy="EUR">250.00</Amt>
-                        </Chrgs>
-                        <RltdPties>
-                            <Dbtr>
-                                <Nm>Mr USA</Nm>
-                                <PstlAdr>
-                                    <Ctry>US</Ctry>
-                                    <AdrLine>42 Some Street</AdrLine>
-                                    <AdrLine>4242 Somewhere</AdrLine>
-                                </PstlAdr>
-                            </Dbtr>
-                            <DbtrAcct>
-                                <Id>
-                                    <Othr>
-                                        <Id>9876543</Id>
-                                    </Othr>
-                                </Id>
-                            </DbtrAcct>
-                        </RltdPties>
-                        <RltdAgts>
-                            <DbtrAgt>
-                                <FinInstnId>
-                                    <BIC>BANKUSNY</BIC>
-                                </FinInstnId>
-                            </DbtrAgt>
-                        </RltdAgts>
-                        <RmtInf>
-                            <Ustrd>Invoice No. 4242</Ustrd>
-                        </RmtInf>
-                    </TxDtls>
-                </NtryDtls>
-                <AddtlNtryInf>AZV-UEBERWEISUNGSGUTSCHRIFT</AddtlNtryInf>
-            </Ntry>
-
-            <Ntry>
-                <Amt Ccy="EUR">48.42</Amt>
-                <CdtDbtInd>DBIT</CdtDbtInd>
-                <Sts>BOOK</Sts>
-                <BookgDt>
-                    <Dt>2020-07-07</Dt>
-                </BookgDt>
-                <ValDt>
-                    <Dt>2020-07-07</Dt>
-                </ValDt>
-                <AcctSvcrRef>acctsvcrref-005</AcctSvcrRef>
-                <BkTxCd>
-                    <Domn>
-                        <Cd>PMNT</Cd>
-                        <Fmly>
-                            <Cd>ICDT</Cd>
-                            <SubFmlyCd>ESCT</SubFmlyCd>
-                        </Fmly>
-                    </Domn>
-                </BkTxCd>
-                <AmtDtls>
-                    <TxAmt>
-                        <Amt Ccy="CHF">46.3</Amt>
-                    </TxAmt>
-                </AmtDtls>
-                <NtryDtls>
-                    <Btch>
-                        <MsgId>UXC20070700006</MsgId>
-                        <PmtInfId>UXC20070700006PI00001</PmtInfId>
-                        <NbOfTxs>2</NbOfTxs>
-                        <TtlAmt Ccy="EUR">46.3</TtlAmt>
-                        <CdtDbtInd>DBIT</CdtDbtInd>
-                    </Btch>
-                    <TxDtls>
-                        <AmtDtls>
-                            <TxAmt>
-                                <Amt Ccy="EUR">23.1</Amt>
-                            </TxAmt>
-                        </AmtDtls>
-                        <BkTxCd>
-                            <Domn>
-                                <Cd>PMNT</Cd>
-                                <Fmly>
-                                    <Cd>ICDT</Cd>
-                                    <SubFmlyCd>ESCT</SubFmlyCd>
-                                </Fmly>
-                            </Domn>
-                        </BkTxCd>
-                        <RltdPties>
-                            <Cdtr>
-                                <Nm>Zahlungsempfaenger 23, ZA 5, DE</Nm>
-                                <PstlAdr>
-                                    <Ctry>DE</Ctry>
-                                    <AdrLine>DE Adresszeile 1</AdrLine>
-                                    <AdrLine>DE Adresszeile 2</AdrLine>
-                                </PstlAdr>
-                            </Cdtr>
-                            <CdtrAcct>
-                                <Id>
-                                    <IBAN>DE32733516350012345678</IBAN>
-                                </Id>
-                            </CdtrAcct>
-                        </RltdPties>
-                        <RltdAgts>
-                            <CdtrAgt>
-                                <FinInstnId>
-                                    <BIC>BYLADEM1ALR</BIC>
-                                </FinInstnId>
-                            </CdtrAgt>
-                        </RltdAgts>
-                    </TxDtls>
-                    <TxDtls>
-                        <Refs>
-                            <MsgId>asdfasdf</MsgId>
-                            <AcctSvcrRef>5j3k453k45</AcctSvcrRef>
-                            <PmtInfId>6j564l56</PmtInfId>
-                            <InstrId>6jl5lj65afasdf</InstrId>
-                            <EndToEndId>jh45k34h5l</EndToEndId>
-                        </Refs>
-                        <AmtDtls>
-                            <TxAmt>
-                                <Amt Ccy="EUR">23.2</Amt>
-                            </TxAmt>
-                        </AmtDtls>
-                        <BkTxCd>
-                            <Domn>
-                                <Cd>PMNT</Cd>
-                                <Fmly>
-                                    <Cd>ICDT</Cd>
-                                    <SubFmlyCd>ESCT</SubFmlyCd>
-                                </Fmly>
-                            </Domn>
-                            <Prtry>
-                                <Cd>K25</Cd>
-                            </Prtry>
-                        </BkTxCd>
-                        <RltdPties>
-                            <Cdtr>
-                                <Nm>Zahlungsempfaenger 23, ZA 5, AT</Nm>
-                                <PstlAdr>
-                                    <Ctry>AT</Ctry>
-                                    <AdrLine>AT Adresszeile 1</AdrLine>
-                                    <AdrLine>AT Adresszeile 2</AdrLine>
-                                </PstlAdr>
-                            </Cdtr>
-                            <CdtrAcct>
-                                <Id>
-                                    <IBAN>AT071100000012345678</IBAN>
-                                </Id>
-                            </CdtrAcct>
-                        </RltdPties>
-                        <RltdAgts>
-                            <CdtrAgt>
-                                <FinInstnId>
-                                    <BIC>BKAUATWW</BIC>
-                                </FinInstnId>
-                            </CdtrAgt>
-                        </RltdAgts>
-                    </TxDtls>
-                </NtryDtls>
-                <AddtlNtryInf>Order</AddtlNtryInf>
-            </Ntry>
-
-        </Stmt>
-    </BkToCstmrStmt>
-</Document>
diff --git a/nexus/src/test/resources/logback-test.xml 
b/nexus/src/test/resources/logback-test.xml
deleted file mode 100644
index cbd96f5f..00000000
--- a/nexus/src/test/resources/logback-test.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<!-- configuration scan="true" -->
-<configuration>
-    <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
-       <target>System.err</target>
-        <encoder>
-            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - 
%msg%n</pattern>
-        </encoder>
-    </appender>
-
-    <logger name="tech.libeufin.nexus" level="ALL"  additivity="false">
-        <appender-ref ref="STDERR" />
-    </logger>
-
-    <logger name="tech.libeufin.sandbox" level="ALL"  additivity="false">
-        <appender-ref ref="STDERR" />
-    </logger>
-
-    <logger name="io.netty" level="WARN"/>
-    <logger name="ktor" level="WARN"/>
-    <logger name="Exposed" level="WARN"/>
-    <logger name="tech.libeufin.util" level="DEBUG"/>
-    <logger name="ch.qos" level="WARN"/>
-
-    <root level="WARN">
-        <appender-ref ref="STDERR"/>
-    </root>
-
-</configuration>
diff --git a/util/build.gradle b/util/build.gradle
index 9c4642b7..38a8ab61 100644
--- a/util/build.gradle
+++ b/util/build.gradle
@@ -58,9 +58,6 @@ dependencies {
     testImplementation group: 'junit', name: 'junit', version: '4.13.2'
     testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21'
     testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21'
-    testImplementation project(":bank")
-    testImplementation project(":nexus")
-
 }
 
 application {
diff --git a/util/src/main/kotlin/Config.kt b/util/src/main/kotlin/Config.kt
index 17ec8a56..ac1c636e 100644
--- a/util/src/main/kotlin/Config.kt
+++ b/util/src/main/kotlin/Config.kt
@@ -64,10 +64,7 @@ fun getValueFromEnv(varName: String): String? {
     return ret
 }
 
-/**
- * Gets the connection string in Postgres format and
- * returns the JDBC version of it.
- */
+// Gets the DB connection string from env, or fail if not found.
 fun getDbConnFromEnv(varName: String): String {
     val dbConnStr = System.getenv(varName)
     if (dbConnStr.isNullOrBlank() or dbConnStr.isNullOrEmpty()) {
diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt
index d61fa2d2..786e2e1e 100644
--- a/util/src/main/kotlin/DB.kt
+++ b/util/src/main/kotlin/DB.kt
@@ -255,12 +255,16 @@ fun connectWithSchema(jdbcConn: String, schemaName: 
String? = null) {
     }
 }
 
+// Prepends "jdbc:" to the Postgres database connection string.
+fun getJdbcConnectionFromPg(pgConn: String): String {
+    return "jdbc:$pgConn"
+}
 /**
  * This function converts a postgresql://-URI to a JDBC one.
  * It is only needed because JDBC strings based on Unix domain
  * sockets need individual intervention.
  */
-fun getJdbcConnectionFromPg(pgConn: String): String {
+fun _getJdbcConnectionFromPg(pgConn: String): String {
     if (!pgConn.startsWith("postgresql://") && 
!pgConn.startsWith("postgres://")) {
         logger.info("Not a Postgres connection string: $pgConn")
         throw internalServerError("Not a Postgres connection string: $pgConn")
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
index c41c6aa2..0e4b05a9 100644
--- a/util/src/main/kotlin/HTTP.kt
+++ b/util/src/main/kotlin/HTTP.kt
@@ -36,7 +36,7 @@ fun badGateway(msg: String): UtilError {
 
 /**
  * Returns the token (including the 'secret-token:' prefix)
- * from a Authorization header.  Throws exception on malformations
+ * from an Authorization header.  Throws exception on malformations
  * Note, the token gets URL-decoded before being returned.
  */
 fun extractToken(authHeader: String): String {
@@ -153,20 +153,14 @@ fun expectAdmin(username: String?) {
 }
 
 fun getHTTPBasicAuthCredentials(request: 
io.ktor.server.request.ApplicationRequest): Pair<String, String> {
-    val authHeader = getAuthorizationHeader(request)
+    val authHeader = getAuthorizationRawHeader(request)
     return extractUserAndPassword(authHeader)
 }
 
-/**
- * Extracts the Authorization:-header line and throws error if not found.
- */
-fun getAuthorizationHeader(request: ApplicationRequest): String {
+// Extracts the Authorization:-header line and throws error if not found.
+fun getAuthorizationRawHeader(request: ApplicationRequest): String {
     val authorization = request.headers["Authorization"]
-    // logger.debug("Found Authorization header: $authorization")
-    return authorization ?: throw UtilError(
-        HttpStatusCode.Unauthorized, "Authorization header not found",
-        LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
-    )
+    return authorization ?: throw badRequest("Authorization header not found")
 }
 
 // Builds the Authorization:-header value, given the credentials.
@@ -176,6 +170,24 @@ fun buildBasicAuthLine(username: String, password: 
String): String {
     val enc = bytesToBase64(cred.toByteArray(Charsets.UTF_8))
     return ret+enc
 }
+
+/**
+ * Holds the details contained in an Authorization header.
+ * The content is held as it was found in the header and supposed
+ * to be processed according to the scheme.
+ */
+data class AuthorizationDetails(
+    val scheme: String,
+    val content: String
+)
+// Returns the authorization scheme mentioned in the Auth header.
+fun getAuthorizationDetails(authorizationHeader: String): AuthorizationDetails 
{
+    val split = authorizationHeader.split(" ")
+    if (split.isEmpty()) throw badRequest("malformed Authorization header: 
contains no space")
+    if (split.size != 2) throw badRequest("malformed Authorization header: 
contains more than one space")
+    return AuthorizationDetails(scheme = split[0], content = split[1])
+}
+
 /**
  * This helper function parses a Authorization:-header line, decode the 
credentials
  * and returns a pair made of username and hashed (sha256) password.  The 
hashed value
diff --git a/util/src/main/kotlin/iban.kt b/util/src/main/kotlin/iban.kt
index b2de9351..76e2fcc5 100644
--- a/util/src/main/kotlin/iban.kt
+++ b/util/src/main/kotlin/iban.kt
@@ -4,10 +4,10 @@ import java.math.BigInteger
 
 fun getIban(): String {
     val ccNoCheck = "131400" // DE00
-    val bban = (0..3).map {
+    val bban = (0..10).map {
         (0..9).random()
     }.joinToString("") // 4 digits BBAN.
-    var checkDigits = 
"98".toBigInteger().minus("$bban$ccNoCheck".toBigInteger().mod("97".toBigInteger())).toString()
+    var checkDigits: String = 
"98".toBigInteger().minus("$bban$ccNoCheck".toBigInteger().mod("97".toBigInteger())).toString()
     if (checkDigits.length == 1) {
         checkDigits = "0${checkDigits}"
     }
diff --git a/util/src/main/kotlin/startServer.kt 
b/util/src/main/kotlin/startServer.kt
index cabd91a7..3d6e9a15 100644
--- a/util/src/main/kotlin/startServer.kt
+++ b/util/src/main/kotlin/startServer.kt
@@ -30,6 +30,8 @@ private fun serverMain(options: StartServerOptions, app: 
Application.() -> Unit)
             }
             module(app)
         },
+        // Maybe remove this?  Was introduced
+        // to debug concurrency issues..
         configure = {
             connectionGroupSize = 1
             workerGroupSize = 1
diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt
index 1dd37df9..ff75158d 100644
--- a/util/src/main/kotlin/time.kt
+++ b/util/src/main/kotlin/time.kt
@@ -30,6 +30,8 @@ fun setClock(rel: Duration) {
 fun getNow(): ZonedDateTime {
     return ZonedDateTime.now(ZoneId.systemDefault())
 }
+
+fun ZonedDateTime.toMicro(): Long = this.nano / 1000L
 fun getNowMillis(): Long = getNow().toInstant().toEpochMilli()
 
 fun getSystemTimeNow(): ZonedDateTime {
diff --git a/util/src/test/kotlin/StartServerTest.kt 
b/util/src/test/kotlin/StartServerTest.kt
deleted file mode 100644
index ef615179..00000000
--- a/util/src/test/kotlin/StartServerTest.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-import org.junit.Ignore
-import org.junit.Test
-import tech.libeufin.nexus.server.nexusApp
-import tech.libeufin.sandbox.sandboxApp
-import tech.libeufin.util.StartServerOptions
-import tech.libeufin.util.startServerWithIPv4Fallback
-
-@Ignore
-class StartServerTest {
-    @Test
-    fun sandboxStart() {
-        startServerWithIPv4Fallback(
-            options = StartServerOptions(
-                ipv4OnlyOpt = false,
-                localhostOnlyOpt = false,
-                portOpt = 5000
-            ),
-            app = sandboxApp
-        )
-    }
-    @Test
-    fun nexusStart() {
-        startServerWithIPv4Fallback(
-            options = StartServerOptions(
-                ipv4OnlyOpt = false,
-                localhostOnlyOpt = true,
-                portOpt = 5000
-            ),
-            app = nexusApp
-        )
-    }
-}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]