gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated: obsolete nexus gone from master


From: gnunet
Subject: [libeufin] branch master updated: obsolete nexus gone from master
Date: Wed, 18 Oct 2023 15:22:53 +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 c414263d obsolete nexus gone from master
c414263d is described below

commit c414263db597bae5a4c29019567b6299cd78553a
Author: MS <ms@taler.net>
AuthorDate: Wed Oct 18 15:22:45 2023 +0200

    obsolete nexus gone from master
---
 nexus/README                                       |   29 -
 nexus/build.gradle                                 |  134 ---
 .../main/kotlin/tech/libeufin/nexus/Anastasis.kt   |  151 ---
 nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt  |   98 --
 .../tech/libeufin/nexus/BankConnectionProtocol.kt  |  105 --
 nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt    |  599 ----------
 .../main/kotlin/tech/libeufin/nexus/DB_helpers.kt  |  123 --
 .../src/main/kotlin/tech/libeufin/nexus/Errors.kt  |   35 -
 .../main/kotlin/tech/libeufin/nexus/FacadeUtil.kt  |  121 --
 .../kotlin/tech/libeufin/nexus/JsonLiterals.kt     |   42 -
 nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt  |  200 ----
 .../main/kotlin/tech/libeufin/nexus/Scheduling.kt  |  172 ---
 nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt |  710 -----------
 .../tech/libeufin/nexus/bankaccount/BankAccount.kt |  456 -------
 .../tech/libeufin/nexus/ebics/EbicsClient.kt       |  437 -------
 .../kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 1247 --------------------
 .../tech/libeufin/nexus/iso20022/GbicRules.kt      |  286 -----
 .../tech/libeufin/nexus/iso20022/Iso20022.kt       | 1023 ----------------
 .../kotlin/tech/libeufin/nexus/server/Helpers.kt   |   79 --
 .../main/kotlin/tech/libeufin/nexus/server/JSON.kt |  444 -------
 .../tech/libeufin/nexus/server/NexusServer.kt      | 1182 -------------------
 .../nexus/server/RequestBodyDecompression.kt       |   47 -
 .../nexus/xlibeufinbank/XLibeufinBankNexus.kt      |  536 ---------
 nexus/src/main/resources/logback.xml               |   23 -
 24 files changed, 8279 deletions(-)

diff --git a/nexus/README b/nexus/README
deleted file mode 100644
index 19af9703..00000000
--- a/nexus/README
+++ /dev/null
@@ -1,29 +0,0 @@
-Description
-===========
-
-The Libeufin Nexus implements a JSON API to let customers manages their
-bank accounts.  The Nexus will then convert those requests sent by customers
-into one of more technical protocols actually implemented by banks; notably,
-EBICS and FinTS.
-
-Running Nexus
-=============
-
-Run the Nexus with the following command
-
-$ cd <top-level directory of this repository>
-$ ./gradlew nexus:run --console=plain --args=serve [--db-name=<my-db>]
-
-Installing the Nexus start script along the project files
-=========================================================
-
-$ cd <top-level directory of this repository>
-$ ./gradlew -q -Pprefix=<installation prefix> nexus:installToPrefix
-
-If the previous step succeeded, the nexus can be launched by the
-following file: "<installation prefix>/bin/libeufin-nexus".
-
-Documentation
-=============
-
-See https://docs.libeufin.tech/ for the documentation.
diff --git a/nexus/build.gradle b/nexus/build.gradle
deleted file mode 100644
index 66ec7f98..00000000
--- a/nexus/build.gradle
+++ /dev/null
@@ -1,134 +0,0 @@
-plugins {
-    id 'kotlin'
-    id 'java'
-    id 'application'
-    id 'org.jetbrains.kotlin.jvm'
-    id "com.github.johnrengelman.shadow" version "5.2.0"
-}
-
-sourceSets {
-    main.java.srcDirs = ['src/main/kotlin']
-}
-
-task installToPrefix(type: Copy) {
-    dependsOn(installShadowDist)
-    from("build/install/nexus-shadow") {
-        include("**/libeufin-nexus")
-        include("**/*.jar")
-    }
-    /**
-     * Reads from command line -Pkey=value options,
-     * with a default (/tmp) if the key is not found.
-     *
-     * project.findProperty('prefix') ?: '/tmp'
-     */
-    into "${project.findProperty('prefix') ?: '/tmp'}"
-}
-
-apply plugin: 'kotlin-kapt'
-
-sourceCompatibility = '11'
-targetCompatibility = '11'
-version = rootProject.version
-
-compileKotlin {
-    kotlinOptions {
-        jvmTarget = '11'
-    }
-}
-
-compileTestKotlin {
-    kotlinOptions {
-        jvmTarget = '11'
-    }
-}
-
-dependencies {
-    // Core language libraries
-    implementation 
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt'
-
-    // LibEuFin util library
-    implementation project(":util")
-
-    // Logging
-    implementation 'ch.qos.logback:logback-classic:1.4.5'
-
-    // XML parsing/binding and encryption
-    implementation "javax.xml.bind:jaxb-api:2.3.0"
-    implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1"
-    implementation 'org.apache.santuario:xmlsec:2.2.2'
-
-    // Compression
-    implementation group: 'org.apache.commons', name: 'commons-compress', 
version: '1.21'
-
-    // Command line parsing
-    implementation('com.github.ajalt:clikt:2.8.0')
-
-    // Exposed, an SQL library
-    implementation "org.jetbrains.exposed:exposed-dao:$exposed_version"
-    implementation "org.jetbrains.exposed:exposed-jdbc:$exposed_version"
-
-    // Database connection driver
-    implementation group: 'org.xerial', name: 'sqlite-jdbc', version: 
'3.36.0.1'
-    implementation 'org.postgresql:postgresql:42.2.23.jre7'
-
-    // Ktor, an HTTP client and server library
-    implementation "io.ktor:ktor-server-core:$ktor_version"
-    implementation "io.ktor:ktor-server-content-negotiation:$ktor_version"
-    implementation "io.ktor:ktor-server-status-pages:$ktor_version"
-    implementation "io.ktor:ktor-client-apache:$ktor_version"
-    implementation "io.ktor:ktor-client-auth:$ktor_version"
-    implementation "io.ktor:ktor-server-netty:$ktor_version"
-
-    // Brings the call-logging library too.
-    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"
-
-    // PDF generation
-    implementation 'com.itextpdf:itext7-core:7.1.16'
-
-    // Cron syntax
-    implementation 'com.cronutils:cron-utils:9.1.5'
-
-    // UNIX domain sockets support (used to connect to PostgreSQL)
-    implementation 'com.kohlschutter.junixsocket:junixsocket-core:2.6.2'
-
-    // Unit testing
-    // testImplementation 'junit:junit:4.13.2'
-    // From 
https://docs.gradle.org/current/userguide/java_testing.html#sec:java_testing_basics:
-    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
-    testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21'
-    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'
-}
-
-test {
-    useJUnit()
-    failFast = true
-    testLogging.showStandardStreams = false
-    environment.put("LIBEUFIN_BANK_ADMIN_PASSWORD", "foo")
-    environment.put("LIBEUFIN_CASHOUT_TEST_TAN", "foo")
-}
-
-application {
-    mainClassName = "tech.libeufin.nexus.MainKt"
-    applicationName = "libeufin-nexus"
-    applicationDefaultJvmArgs = ['-Djava.net.preferIPv6Addresses=true']
-}
-
-jar {
-    manifest {
-        attributes "Main-Class": "tech.libeufin.nexus.MainKt"
-    }
-}
-
-run {
-    standardInput = System.in
-}
-
-task pofi(type: JavaExec) {
-    classpath = sourceSets.test.runtimeClasspath
-    mainClass = "PostFinanceKt"
-}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt
deleted file mode 100644
index 49e512fe..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt
+++ /dev/null
@@ -1,151 +0,0 @@
-package tech.libeufin.nexus
-
-import TransactionDetails
-import io.ktor.http.*
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.server.PermissionQuery
-import tech.libeufin.nexus.server.expectNonNull
-import tech.libeufin.nexus.server.expectUrlParameter
-import tech.libeufin.util.EbicsProtocolError
-import kotlin.math.abs
-import kotlin.math.min
-import io.ktor.content.TextContent
-import io.ktor.server.application.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import tech.libeufin.util.buildIbanPaytoUri
-import tech.libeufin.util.internalServerError
-
-data class AnastasisIncomingBankTransaction(
-    val row_id: Long,
-    val date: GnunetTimestamp, // timestamp
-    val amount: String,
-    val debit_account: String,
-    val subject: String
-)
-
-fun anastasisFilter(payment: NexusBankTransactionEntity, txDtls: 
TransactionDetails) {
-    val debtorName = txDtls.debtor?.name
-    if (debtorName == null) {
-        logger.warn("empty debtor name")
-        return
-    }
-    val debtorAcct = txDtls.debtorAccount
-    if (debtorAcct == null) {
-        // FIXME: Report payment, we can't even send it back
-        logger.warn("empty debtor account")
-        return
-    }
-    val debtorIban = debtorAcct.iban
-    if (debtorIban == null) {
-        // FIXME: Report payment, we can't even send it back
-        logger.warn("non-iban debtor account")
-        return
-    }
-    val debtorAgent = txDtls.debtorAgent
-    if (debtorAgent == null) {
-        // FIXME: Report payment, we can't even send it back
-        logger.warn("missing debtor agent")
-        return
-    }
-    /**
-     * This block either assigns a non-null BIC to the 'bic'
-     * variable, or causes this function (anastasisFilter())
-     * to return.  This last action ensures that the payment
-     * being processed won't show up in the Anastasis facade.
-     */
-    val bic: String = debtorAgent.bic ?: run {
-        logger.warn("Not allowing transactions missing the BIC.  IBAN and 
name: ${debtorIban}, $debtorName")
-        return
-    }
-    val paymentSubject = txDtls.unstructuredRemittanceInformation
-    if (paymentSubject == null) {
-        throw internalServerError("Nexus payment 
'${payment.accountTransactionId}' has no subject.")
-    }
-    AnastasisIncomingPaymentEntity.new {
-        this.payment = payment
-        subject = paymentSubject
-        timestampMs = System.currentTimeMillis()
-        debtorPaytoUri = buildIbanPaytoUri(
-            debtorIban,
-            bic,
-            debtorName,
-        )
-    }
-}
-
-data class AnastasisIncomingTransactions(
-    val credit_account: String,
-    val incoming_transactions: MutableList<AnastasisIncomingBankTransaction>
-)
-
-// Handle a /taler-wire-gateway/history/incoming request.
-private suspend fun historyIncoming(call: ApplicationCall) {
-    val facadeId = expectNonNull(call.parameters["fcid"])
-    call.request.requirePermission(
-        PermissionQuery(
-            "facade",
-            facadeId,
-            "facade.anastasis.history"
-        )
-    )
-    val param = call.expectUrlParameter("delta")
-    val delta: Int = try { param.toInt() } catch (e: Exception) {
-        throw EbicsProtocolError(HttpStatusCode.BadRequest, "'${param}' is not 
Int")
-    }
-    val start: Long = handleStartArgument(
-        call.request.queryParameters["start"],
-        delta
-    )
-    val history = object {
-        val incoming_transactions: 
MutableList<AnastasisIncomingBankTransaction> = mutableListOf()
-
-    }
-    val startCmpOp = getComparisonOperator(delta, start, 
AnastasisIncomingPaymentsTable)
-    val incomingTransactionsResp = transaction {
-        val orderedPayments = AnastasisIncomingPaymentEntity.find {
-            startCmpOp
-        }.orderTaler(delta) // Taler and Anastasis have same ordering policy.  
Fixme: find better function's name?
-        if (orderedPayments.isNotEmpty()) {
-            val creditBankAccountObj = orderedPayments[0]
-            val ret = AnastasisIncomingTransactions(
-                credit_account = buildIbanPaytoUri(
-                    creditBankAccountObj.payment.bankAccount.iban,
-                    creditBankAccountObj.payment.bankAccount.bankCode,
-                    creditBankAccountObj.payment.bankAccount.accountHolder,
-                ),
-                incoming_transactions = mutableListOf()
-            )
-            orderedPayments.subList(0, min(abs(delta), 
orderedPayments.size)).forEach {
-                history.incoming_transactions.add(
-                    AnastasisIncomingBankTransaction(
-                        // Rounded timestamp
-                        date = GnunetTimestamp(it.timestampMs / 1000L),
-                        row_id = it.id.value,
-                        amount = "${it.payment.currency}:${it.payment.amount}",
-                        subject = it.subject,
-                        debit_account = it.debtorPaytoUri
-                    )
-                )
-            }
-            return@transaction ret
-        } else null
-    }
-    if (incomingTransactionsResp == null) {
-        call.respond(HttpStatusCode.NoContent)
-        return
-    }
-    return call.respond(
-        TextContent(
-            customConverter(incomingTransactionsResp),
-            ContentType.Application.Json
-        )
-    )
-}
-
-fun anastasisFacadeRoutes(route: Route) {
-    route.get("/history/incoming") {
-        historyIncoming(call)
-        return@get
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
deleted file mode 100644
index 2048813e..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-package tech.libeufin.nexus
-
-import UtilError
-import io.ktor.http.*
-import io.ktor.server.request.*
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.server.Permission
-import tech.libeufin.nexus.server.PermissionQuery
-import tech.libeufin.util.*
-
-fun getNexusUser(username: String): NexusUserEntity =
-    transaction {
-        NexusUserEntity.find {
-            NexusUsersTable.username eq username
-        }.firstOrNull() ?: throw notFound("User $username not found.")
-    }
-
-/**
- * HTTP basic auth.  Throws error if password is wrong,
- * and makes sure that the user exists in the system.
- *
- * @return user entity
- */
-fun authenticateRequest(request: ApplicationRequest): NexusUserEntity {
-    return transaction {
-        val (username, password) = getHTTPBasicAuthCredentials(request)
-        val user = NexusUserEntity.find {
-            NexusUsersTable.username eq username
-        }.firstOrNull()
-        if (user == null) {
-            throw UtilError(HttpStatusCode.Unauthorized,
-                "Unknown user '$username'",
-                LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
-            )
-        }
-        CryptoUtil.checkPwOrThrow(password, user.passwordHash)
-        user
-    }
-}
-
-fun requireSuperuser(request: ApplicationRequest): NexusUserEntity {
-    return transaction {
-        val user = authenticateRequest(request)
-        if (!user.superuser) {
-            throw NexusError(HttpStatusCode.Forbidden, "must be superuser")
-        }
-        user
-    }
-}
-
-fun findPermission(p: Permission): NexusPermissionEntity? {
-    return transaction {
-        NexusPermissionEntity.find {
-            ((NexusPermissionsTable.subjectType eq p.subjectType)
-                    and (NexusPermissionsTable.subjectId eq p.subjectId)
-                    and (NexusPermissionsTable.resourceType eq p.resourceType)
-                    and (NexusPermissionsTable.resourceId eq p.resourceId)
-                    and (NexusPermissionsTable.permissionName eq 
p.permissionName.lowercase()))
-
-        }.firstOrNull()
-    }
-}
-
-
-/**
- * Require that the authenticated user has at least one of the listed 
permissions.
- *
- * Throws a NexusError if the authenticated user for the request doesn't have 
any of
- * listed the permissions.  It returns the username of the authorized user.
- */
-fun ApplicationRequest.requirePermission(vararg perms: PermissionQuery): 
String {
-    val username = transaction {
-        val user = authenticateRequest(this@requirePermission)
-        if (user.superuser) {
-            return@transaction user.username
-        }
-        var foundPermission = false
-        for (pr in perms) {
-            val p = Permission("user", user.username, pr.resourceType, 
pr.resourceId, pr.permissionName.lowercase())
-            val existingPerm = findPermission(p)
-            if (existingPerm != null) {
-                foundPermission = true
-                break
-            }
-        }
-        if (!foundPermission) {
-            val possiblePerms =
-                perms.joinToString(" | ") { "${it.resourceId} 
${it.resourceType} ${it.permissionName}" }
-            throw NexusError(
-                HttpStatusCode.Forbidden,
-                "User ${user.username} has insufficient permissions (needs 
$possiblePerms)."
-            )
-        }
-        user.username
-    }
-    return username
-}
\ No newline at end of file
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
deleted file mode 100644
index 8607d3c9..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
+++ /dev/null
@@ -1,105 +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.nexus
-
-import com.fasterxml.jackson.databind.JsonNode
-import io.ktor.client.HttpClient
-import io.ktor.http.HttpStatusCode
-import tech.libeufin.nexus.ebics.*
-import tech.libeufin.nexus.server.BankConnectionType
-import tech.libeufin.nexus.server.FetchSpecJson
-import tech.libeufin.nexus.server.XLibeufinBankTransport
-import tech.libeufin.nexus.xlibeufinbank.XlibeufinBankConnectionProtocol
-
-// 'const' allows only primitive types.
-val bankConnectionRegistry: Map<BankConnectionType, BankConnectionProtocol> = 
mapOf(
-    BankConnectionType.EBICS to EbicsBankConnectionProtocol(),
-    BankConnectionType.X_LIBEUFIN_BANK to XlibeufinBankConnectionProtocol()
-)
-
-interface BankConnectionProtocol {
-    // Get the bank URL in the same format that
-    // it was given when the connection was created.
-    // This helps the /admin/add-incoming handler.
-    fun getBankUrl(connId: String): String
-    // Initialize the connection.  Usually uploads keys to the bank.
-    suspend fun connect(client: HttpClient, connId: String)
-
-    // Downloads the list of bank accounts managed at the
-    // bank under one particular connection.
-    suspend fun fetchAccounts(client: HttpClient, connId: String)
-
-    // Create a new connection from backup data.
-    fun createConnectionFromBackup(connId: String, user: NexusUserEntity, 
passphrase: String?, backup: JsonNode)
-
-    // Create a new connection from an HTTP request.
-    fun createConnection(connId: String, user: NexusUserEntity, data: JsonNode)
-
-    // Merely a formatter of connection details coming from
-    // the database.
-    fun getConnectionDetails(conn: NexusBankConnectionEntity): JsonNode
-
-    // Returns the backup data.
-    fun exportBackup(bankConnectionId: String, passphrase: String): JsonNode
-
-    // Export a printable format of the connection details.  Useful
-    // to provide authentication via the traditional mail system.
-    fun exportAnalogDetails(conn: NexusBankConnectionEntity): ByteArray
-
-    // Send to the bank a previously prepared payment instruction.
-    suspend fun submitPaymentInitiation(httpClient: HttpClient, 
paymentInitiationId: Long)
-
-    /**
-     * Downloads transactions from the bank, according to the specification
-     * given in the arguments.
-     *
-     * This function returns a possibly empty list of exceptions.
-     * That helps not to stop fetching if ONE operation fails.  Notably,
-     * C52 _and_ C53 may be asked along one invocation of this function,
-     * therefore storing the exception on C52 allows the C53 to still
-     * take place.  The caller then decides how to handle the exceptions.
-     *
-     * More on multi requests: C52 and C53, or more generally 'reports'
-     * and 'statements' are tried to be downloaded together when the fetch
-     * level is set to ALL.
-     */
-    suspend fun fetchTransactions(
-        fetchSpec: FetchSpecJson,
-        client: HttpClient,
-        bankConnectionId: String,
-        accountId: String
-    ): List<Exception>?
-}
-
-fun getConnectionPlugin(connType: BankConnectionType): BankConnectionProtocol {
-    return bankConnectionRegistry[connType] ?: throw NexusError(
-        HttpStatusCode.NotFound,
-        "Connection type '${connType}' not available"
-    )
-}
-
-/**
- * Adaptor helper to keep until all the connection type mentions will
- * be passed as BankConnectionType instead of arbitrary easy-to-break
- * string.
- */
-fun getConnectionPlugin(connType: String): BankConnectionProtocol {
-    return 
getConnectionPlugin(BankConnectionType.parseBankConnectionType(connType))
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
deleted file mode 100644
index c0d712fd..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ /dev/null
@@ -1,599 +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.nexus
-
-import EntryStatus
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import org.jetbrains.exposed.dao.*
-import org.jetbrains.exposed.dao.id.EntityID
-import org.jetbrains.exposed.dao.id.LongIdTable
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.transactions.TransactionManager
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.server.FetchLevel
-import tech.libeufin.util.*
-
-/**
- * This table holds the values that exchange gave to issue a payment,
- * plus a reference to the prepared pain.001 version of.  Note that
- * whether a pain.001 document was sent or not to the bank is indicated
- * in the PAIN-table.
- */
-object TalerRequestedPaymentsTable : LongIdTable() {
-    val facade = reference("facade", FacadesTable)
-    val preparedPayment = reference("payment", PaymentInitiationsTable)
-    val requestUid = text("requestUid")
-    val amount = text("amount")
-    val exchangeBaseUrl = text("exchangeBaseUrl")
-    val wtid = text("wtid")
-    val creditAccount = text("creditAccount")
-}
-
-class TalerRequestedPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<TalerRequestedPaymentEntity>(TalerRequestedPaymentsTable)
-
-    var facade by FacadeEntity referencedOn TalerRequestedPaymentsTable.facade
-    var preparedPayment by PaymentInitiationEntity referencedOn 
TalerRequestedPaymentsTable.preparedPayment
-    var requestUid by TalerRequestedPaymentsTable.requestUid
-    var amount by TalerRequestedPaymentsTable.amount
-    var exchangeBaseUrl by TalerRequestedPaymentsTable.exchangeBaseUrl
-    var wtid by TalerRequestedPaymentsTable.wtid
-    var creditAccount by TalerRequestedPaymentsTable.creditAccount
-}
-
-object TalerInvalidIncomingPaymentsTable : LongIdTable() {
-    val payment = reference("payment", NexusBankTransactionsTable)
-    val timestampMs = long("timestampMs")
-    val refunded = bool("refunded").default(false)
-}
-
-class TalerInvalidIncomingPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<TalerInvalidIncomingPaymentEntity>(TalerInvalidIncomingPaymentsTable)
-
-    var payment by NexusBankTransactionEntity referencedOn 
TalerInvalidIncomingPaymentsTable.payment
-    var timestampMs by TalerInvalidIncomingPaymentsTable.timestampMs
-    // FIXME:  This should probably not be called refunded, and
-    // we should have a foreign key to the payment that sends the
-    // money back.
-    var refunded by TalerInvalidIncomingPaymentsTable.refunded
-}
-
-object AnastasisIncomingPaymentsTable: LongIdTable() {
-    val payment = reference("payment", NexusBankTransactionsTable)
-    val subject = text("subject")
-    val timestampMs = long("timestampMs")
-    val debtorPaytoUri = text("incomingPaytoUri")
-}
-
-class AnastasisIncomingPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<AnastasisIncomingPaymentEntity>(AnastasisIncomingPaymentsTable)
-
-    var payment by NexusBankTransactionEntity referencedOn 
AnastasisIncomingPaymentsTable.payment
-    var subject by AnastasisIncomingPaymentsTable.subject
-    var timestampMs by AnastasisIncomingPaymentsTable.timestampMs
-    var debtorPaytoUri by AnastasisIncomingPaymentsTable.debtorPaytoUri
-}
-
-/**
- * This is the table of the incoming payments.  Entries are merely "pointers" 
to the
- * entries from the raw payments table.
- */
-object TalerIncomingPaymentsTable : LongIdTable() {
-    val payment = reference("payment", NexusBankTransactionsTable)
-    val reservePublicKey = text("reservePublicKey")
-    val timestampMs = long("timestampMs")
-    val debtorPaytoUri = text("incomingPaytoUri")
-}
-
-class TalerIncomingPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<TalerIncomingPaymentEntity>(TalerIncomingPaymentsTable)
-
-    var payment by NexusBankTransactionEntity referencedOn 
TalerIncomingPaymentsTable.payment
-    var reservePublicKey by TalerIncomingPaymentsTable.reservePublicKey
-    var timestampMs by TalerIncomingPaymentsTable.timestampMs
-    var debtorPaytoUri by TalerIncomingPaymentsTable.debtorPaytoUri
-}
-
-/**
- * This table logs all the balances as returned by the bank for all the bank 
accounts.
- */
-object NexusBankBalancesTable : LongIdTable() {
-    /**
-     * Balance mentioned in the bank message referenced below.  NOTE: this is 
the
-     * CLOSING balance (a.k.a. CLBD), namely the one obtained by adding the 
transactions
-     * reported in the bank message to the _previous_ CLBD.
-     */
-    val balance = text("balance") // $currency:x.y
-    val creditDebitIndicator = text("creditDebitIndicator") // CRDT or DBIT.
-    val bankAccount = reference("bankAccount", NexusBankAccountsTable)
-    val date = text("date") // in the YYYY-MM-DD format
-}
-
-class NexusBankBalanceEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<NexusBankBalanceEntity>(NexusBankBalancesTable)
-    var balance by NexusBankBalancesTable.balance
-    var creditDebitIndicator by NexusBankBalancesTable.creditDebitIndicator
-    var bankAccount by NexusBankAccountEntity referencedOn 
NexusBankBalancesTable.bankAccount
-    var date by NexusBankBalancesTable.date
-}
-
-// This table holds the data to talk to Sandbox
-// via the x-libeufin-bank protocol supplier.
-object XLibeufinBankUsersTable : LongIdTable() {
-    val username = text("username")
-    val password = text("password")
-    val baseUrl = text("baseUrl")
-    val nexusBankConnection = reference("nexusBankConnection", 
NexusBankConnectionsTable)
-}
-
-class XLibeufinBankUserEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<XLibeufinBankUserEntity>(XLibeufinBankUsersTable)
-    var username by XLibeufinBankUsersTable.username
-    var password by XLibeufinBankUsersTable.password
-    var baseUrl by XLibeufinBankUsersTable.baseUrl
-    var nexusBankConnection by NexusBankConnectionEntity referencedOn 
XLibeufinBankUsersTable.nexusBankConnection
-}
-
-/**
- * Table that stores all messages we receive from the bank.
- * The nullable fields were introduced along the x-libeufin-bank
- * connection, as those messages are plain JSON object unlike
- * the more structured CaMt.
- */
-object NexusBankMessagesTable : LongIdTable() {
-    val bankConnection = reference("bankConnection", NexusBankConnectionsTable)
-    val message = blob("message")
-    val messageId = text("messageId").nullable()
-    val fetchLevel = enumerationByName("fetchLevel", 16, FetchLevel::class)
-    // true when the parser could not ingest one message:
-    val errors = bool("errors").default(false)
-}
-
-class NexusBankMessageEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<NexusBankMessageEntity>(NexusBankMessagesTable)
-    var bankConnection by NexusBankConnectionEntity referencedOn 
NexusBankMessagesTable.bankConnection
-    var messageId by NexusBankMessagesTable.messageId
-    var fetchLevel by NexusBankMessagesTable.fetchLevel
-    var message by NexusBankMessagesTable.message
-    var errors by NexusBankMessagesTable.errors
-}
-
-/**
- * This table contains history "elements" as returned by the bank from a
- * CAMT message.
- */
-object NexusBankTransactionsTable : LongIdTable() {
-    /**
-     * Identifier for the transaction that is unique among all transactions of 
the account.
-     * The scheme for this identifier is the accounts transaction 
identification scheme.
-     *
-     * Note that this is *not* a unique ID per account, as the same underlying
-     * transaction can show up multiple times with a different status.
-     */
-    val accountTransactionId = text("accountTransactionId")
-    val bankAccount = reference("bankAccount", NexusBankAccountsTable)
-    val creditDebitIndicator = text("creditDebitIndicator")
-    val currency = text("currency")
-    val amount = text("amount")
-    val status = enumerationByName("status", 16, EntryStatus::class)
-    // Another, later transaction that updates the status of the current 
transaction.
-    val updatedBy = optReference("updatedBy", NexusBankTransactionsTable)
-    val transactionJson = text("transactionJson")
-}
-
-class NexusBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<NexusBankTransactionEntity>(NexusBankTransactionsTable) {
-        override fun new(init: NexusBankTransactionEntity.() -> Unit): 
NexusBankTransactionEntity {
-            val ret = super.new(init)
-            if (isPostgres()) {
-                val channelName = buildChannelName(
-                    NotificationsChannelDomains.LIBEUFIN_NEXUS_TX,
-                    ret.bankAccount.bankAccountName
-                )
-                TransactionManager.current().postgresNotify(channelName, 
ret.creditDebitIndicator)
-            }
-            return ret
-        }
-    }
-    var currency by NexusBankTransactionsTable.currency
-    var amount by NexusBankTransactionsTable.amount
-    var status by NexusBankTransactionsTable.status
-    var creditDebitIndicator by NexusBankTransactionsTable.creditDebitIndicator
-    var bankAccount by NexusBankAccountEntity referencedOn 
NexusBankTransactionsTable.bankAccount
-    var transactionJson by NexusBankTransactionsTable.transactionJson
-    var accountTransactionId by NexusBankTransactionsTable.accountTransactionId
-    val updatedBy by NexusBankTransactionEntity optionalReferencedOn 
NexusBankTransactionsTable.updatedBy
-
-    /**
-     * It is responsibility of the caller to insert only valid
-     * JSON into the database, and therefore provide error management
-     * when calling the two helpers below.
-     */
-
-    inline fun <reified T> parseDetailsIntoObject(): T {
-        val mapper = jacksonObjectMapper()
-        return mapper.readValue(this.transactionJson, T::class.java)
-    }
-    fun parseDetailsIntoObject(): JsonNode {
-        val mapper = jacksonObjectMapper()
-        return mapper.readTree(this.transactionJson)
-    }
-}
-
-// Represents a prepared payment.
-object PaymentInitiationsTable : LongIdTable() {
-    /**
-     * Bank account that wants to initiate the payment.
-     */
-    val bankAccount = reference("bankAccount", NexusBankAccountsTable)
-    val preparationDate = long("preparationDate")
-    val submissionDate = long("submissionDate").nullable()
-    val sum = text("sum") // the amount to transfer.
-    val currency = text("currency")
-    val endToEndId = text("endToEndId")
-    val paymentInformationId = text("paymentInformationId")
-    val instructionId = text("instructionId")
-    val subject = text("subject")
-    val creditorIban = text("creditorIban")
-    val creditorBic = text("creditorBic").nullable()
-    val creditorName = text("creditorName")
-    val submitted = bool("submitted").default(false)
-    var invalid = bool("invalid").nullable()
-    val messageId = text("messageId")
-    /**
-     * Points at the raw transaction witnessing that this
-     * initiated payment was successfully performed.
-     */
-    val confirmationTransaction = reference("rawConfirmation", 
NexusBankTransactionsTable).nullable()
-}
-
-class PaymentInitiationEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<PaymentInitiationEntity>(PaymentInitiationsTable)
-
-    var bankAccount by NexusBankAccountEntity referencedOn 
PaymentInitiationsTable.bankAccount
-    var preparationDate by PaymentInitiationsTable.preparationDate
-    var submissionDate by PaymentInitiationsTable.submissionDate
-    var sum by PaymentInitiationsTable.sum
-    var currency by PaymentInitiationsTable.currency
-    var endToEndId by PaymentInitiationsTable.endToEndId
-    var subject by PaymentInitiationsTable.subject
-    var creditorIban by PaymentInitiationsTable.creditorIban
-    var creditorBic by PaymentInitiationsTable.creditorBic
-    var creditorName by PaymentInitiationsTable.creditorName
-    var submitted by PaymentInitiationsTable.submitted
-    var invalid by PaymentInitiationsTable.invalid
-    var paymentInformationId by PaymentInitiationsTable.paymentInformationId
-    var instructionId by PaymentInitiationsTable.instructionId
-    var messageId by PaymentInitiationsTable.messageId
-    var confirmationTransaction by NexusBankTransactionEntity 
optionalReferencedOn PaymentInitiationsTable.confirmationTransaction
-}
-
-/**
- * This table contains the bank accounts that are offered by the bank.
- * The bank account label (as assigned by the bank) is the primary key.
- */
-object OfferedBankAccountsTable : LongIdTable() {
-    val offeredAccountId = text("offeredAccountId")
-    val bankConnection = reference("bankConnection", NexusBankConnectionsTable)
-    val iban = text("iban")
-    val bankCode = text("bankCode")
-    val accountHolder = text("holderName")
-
-    // column below gets defined only WHEN the user imports the bank account.
-    val imported = reference("imported", NexusBankAccountsTable).nullable()
-
-    init {
-        uniqueIndex(offeredAccountId, bankConnection)
-    }
-}
-
-class OfferedBankAccountEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<OfferedBankAccountEntity>(OfferedBankAccountsTable)
-
-    var offeredAccountId by OfferedBankAccountsTable.offeredAccountId
-    var bankConnection by NexusBankConnectionEntity referencedOn 
OfferedBankAccountsTable.bankConnection
-    var accountHolder by OfferedBankAccountsTable.accountHolder
-    var iban by OfferedBankAccountsTable.iban
-    var bankCode by OfferedBankAccountsTable.bankCode
-    var imported by NexusBankAccountEntity optionalReferencedOn  
OfferedBankAccountsTable.imported
-}
-
-/**
- * This table holds triples of <iban, bic, holder name>.
- * FIXME(dold):  Allow other account and bank identifications than IBAN and BIC
- */
-object NexusBankAccountsTable : LongIdTable() {
-    val bankAccountName = text("bankAccountId").uniqueIndex()
-    val accountHolder = text("accountHolder")
-    val iban = text("iban")
-    val bankCode = text("bankCode")
-    val defaultBankConnection = reference("defaultBankConnection", 
NexusBankConnectionsTable).nullable()
-    val lastStatementCreationTimestamp = 
long("lastStatementCreationTimestamp").nullable()
-    val lastReportCreationTimestamp = 
long("lastReportCreationTimestamp").nullable()
-    val lastNotificationCreationTimestamp = 
long("lastNotificationCreationTimestamp").nullable()
-    // Highest bank message ID that this bank account is aware of.
-    val highestSeenBankMessageSerialId = long("highestSeenBankMessageSerialId")
-    val pain001Counter = long("pain001counter").default(1)
-}
-
-class NexusBankAccountEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<NexusBankAccountEntity>(NexusBankAccountsTable) {
-        fun findByName(name: String): NexusBankAccountEntity? {
-            return find { NexusBankAccountsTable.bankAccountName eq name 
}.firstOrNull()
-        }
-    }
-
-    var bankAccountName by NexusBankAccountsTable.bankAccountName
-    var accountHolder by NexusBankAccountsTable.accountHolder
-    var iban by NexusBankAccountsTable.iban
-    var bankCode by NexusBankAccountsTable.bankCode
-    var defaultBankConnection by NexusBankConnectionEntity 
optionalReferencedOn NexusBankAccountsTable.defaultBankConnection
-    var highestSeenBankMessageSerialId by 
NexusBankAccountsTable.highestSeenBankMessageSerialId
-    var pain001Counter by NexusBankAccountsTable.pain001Counter
-    var lastStatementCreationTimestamp by 
NexusBankAccountsTable.lastStatementCreationTimestamp
-    var lastReportCreationTimestamp by 
NexusBankAccountsTable.lastReportCreationTimestamp
-    var lastNotificationCreationTimestamp by 
NexusBankAccountsTable.lastNotificationCreationTimestamp
-}
-
-object NexusEbicsSubscribersTable : LongIdTable() {
-    val ebicsURL = text("ebicsURL")
-    val hostID = text("hostID")
-    val partnerID = text("partnerID")
-    val userID = text("userID")
-    val systemID = text("systemID").nullable()
-    val signaturePrivateKey = blob("signaturePrivateKey")
-    val encryptionPrivateKey = blob("encryptionPrivateKey")
-    val authenticationPrivateKey = blob("authenticationPrivateKey")
-    val bankEncryptionPublicKey = blob("bankEncryptionPublicKey").nullable()
-    val bankAuthenticationPublicKey = 
blob("bankAuthenticationPublicKey").nullable()
-    val nexusBankConnection = reference("nexusBankConnection", 
NexusBankConnectionsTable)
-    val ebicsIniState = enumerationByName("ebicsIniState", 16, 
EbicsInitState::class)
-    val ebicsHiaState = enumerationByName("ebicsHiaState", 16, 
EbicsInitState::class)
-}
-
-class EbicsSubscriberEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<EbicsSubscriberEntity>(NexusEbicsSubscribersTable)
-
-    var ebicsURL by NexusEbicsSubscribersTable.ebicsURL
-    var hostID by NexusEbicsSubscribersTable.hostID
-    var partnerID by NexusEbicsSubscribersTable.partnerID
-    var userID by NexusEbicsSubscribersTable.userID
-    var systemID by NexusEbicsSubscribersTable.systemID
-    var signaturePrivateKey by NexusEbicsSubscribersTable.signaturePrivateKey
-    var encryptionPrivateKey by NexusEbicsSubscribersTable.encryptionPrivateKey
-    var authenticationPrivateKey by 
NexusEbicsSubscribersTable.authenticationPrivateKey
-    var bankEncryptionPublicKey by 
NexusEbicsSubscribersTable.bankEncryptionPublicKey
-    var bankAuthenticationPublicKey by 
NexusEbicsSubscribersTable.bankAuthenticationPublicKey
-    var nexusBankConnection by NexusBankConnectionEntity referencedOn 
NexusEbicsSubscribersTable.nexusBankConnection
-    var ebicsIniState by NexusEbicsSubscribersTable.ebicsIniState
-    var ebicsHiaState by NexusEbicsSubscribersTable.ebicsHiaState
-}
-
-object NexusUsersTable : LongIdTable() {
-    val username = text("username")
-    val passwordHash = text("password")
-    val superuser = bool("superuser")
-}
-
-class NexusUserEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : LongEntityClass<NexusUserEntity>(NexusUsersTable)
-
-    var username by NexusUsersTable.username
-    var passwordHash by NexusUsersTable.passwordHash
-    var superuser by NexusUsersTable.superuser
-}
-
-object NexusBankConnectionsTable : LongIdTable() {
-    val connectionId = text("connectionId")
-    val type = text("type")
-    val dialect = text("dialect").nullable()
-    val owner = reference("user", NexusUsersTable)
-}
-
-class NexusBankConnectionEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<NexusBankConnectionEntity>(NexusBankConnectionsTable) {
-        fun findByName(name: String): NexusBankConnectionEntity? {
-            return find { NexusBankConnectionsTable.connectionId eq name 
}.firstOrNull()
-        }
-    }
-
-    var connectionId by NexusBankConnectionsTable.connectionId
-    var type by NexusBankConnectionsTable.type
-    var dialect by NexusBankConnectionsTable.dialect
-    var owner by NexusUserEntity referencedOn NexusBankConnectionsTable.owner
-}
-
-object FacadesTable : LongIdTable() {
-    val facadeName = text("facadeName")
-    val type = text("type")
-    val creator = reference("creator", NexusUsersTable)
-    init { uniqueIndex(facadeName) }
-}
-
-class FacadeEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : LongEntityClass<FacadeEntity>(FacadesTable) {
-        fun findByName(name: String): FacadeEntity? {
-            return find { FacadesTable.facadeName eq name}.firstOrNull()
-        }
-    }
-    var facadeName by FacadesTable.facadeName
-    var type by FacadesTable.type
-    var creator by NexusUserEntity referencedOn FacadesTable.creator
-}
-
-object FacadeStateTable : LongIdTable() {
-    val bankAccount = text("bankAccount")
-    val bankConnection = text("bankConnection")
-    val currency = text("currency")
-
-    // "statement", "report", "notification"
-    val reserveTransferLevel = text("reserveTransferLevel")
-    val facade = reference("facade", FacadesTable, onDelete = 
ReferenceOption.CASCADE)
-
-    /**
-     * Highest ID seen in the raw transactions table.
-     */
-    val highestSeenMsgSerialId = long("highestSeenMessageSerialId").default(0)
-}
-
-class FacadeStateEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : LongEntityClass<FacadeStateEntity>(FacadeStateTable)
-
-    var bankAccount by FacadeStateTable.bankAccount
-    var bankConnection by FacadeStateTable.bankConnection
-    var currency by FacadeStateTable.currency
-
-    /**
-     *  "statement", "report", "notification"
-     */
-    var reserveTransferLevel by FacadeStateTable.reserveTransferLevel
-    var facade by FacadeEntity referencedOn FacadeStateTable.facade
-    var highestSeenMessageSerialId by FacadeStateTable.highestSeenMsgSerialId
-}
-
-object NexusScheduledTasksTable : LongIdTable() {
-    val resourceType = text("resourceType")
-    val resourceId = text("resourceId")
-    val taskName = text("taskName")
-    val taskType = text("taskType")
-    val taskCronspec = text("taskCronspec")
-    val taskParams = text("taskParams")
-    val nextScheduledExecutionSec = 
long("nextScheduledExecutionSec").nullable()
-    val prevScheduledExecutionSec = 
long("lastScheduledExecutionSec").nullable()
-}
-
-class NexusScheduledTaskEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<NexusScheduledTaskEntity>(NexusScheduledTasksTable)
-
-    var resourceType by NexusScheduledTasksTable.resourceType
-    var resourceId by NexusScheduledTasksTable.resourceId
-    var taskName by NexusScheduledTasksTable.taskName
-    var taskType by NexusScheduledTasksTable.taskType
-    var taskCronspec by NexusScheduledTasksTable.taskCronspec
-    var taskParams by NexusScheduledTasksTable.taskParams
-    var nextScheduledExecutionSec by 
NexusScheduledTasksTable.nextScheduledExecutionSec
-    var prevScheduledExecutionSec by 
NexusScheduledTasksTable.prevScheduledExecutionSec
-}
-
-/**
- * Generic permissions table that determines access of a subject
- * identified by (subjectType, subjectName) to a resource (resourceType, 
resourceId).
- *
- * Subjects are typically of type "user", but this may change in the future.
- */
-object NexusPermissionsTable : LongIdTable() {
-    val resourceType = text("resourceType")
-    val resourceId = text("resourceId")
-    val subjectType = text("subjectType")
-    val subjectId = text("subjectName")
-    val permissionName = text("permissionName")
-
-    init { uniqueIndex(resourceType, resourceId, subjectType, subjectId, 
permissionName) }
-}
-
-class NexusPermissionEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<NexusPermissionEntity>(NexusPermissionsTable)
-
-    var resourceType by NexusPermissionsTable.resourceType
-    var resourceId by NexusPermissionsTable.resourceId
-    var subjectType by NexusPermissionsTable.subjectType
-    var subjectId by NexusPermissionsTable.subjectId
-    var permissionName by NexusPermissionsTable.permissionName
-}
-
-fun dbDropTables(connStringFromEnv: String) {
-    connectWithSchema(getJdbcConnectionFromPg(connStringFromEnv))
-    if (isPostgres()) {
-        val ret = execCommand(
-            listOf(
-                "libeufin-load-sql",
-                "-d",
-                connStringFromEnv,
-                "-s",
-                "nexus",
-                "-r"
-            ),
-            throwIfFails = false
-        )
-        if (ret != 0)
-            logger.warn("Dropping the nexus tables failed.  Was the DB filled 
before?")
-        return
-    }
-    transaction {
-        SchemaUtils.drop(
-            NexusUsersTable,
-            XLibeufinBankUsersTable,
-            PaymentInitiationsTable,
-            NexusEbicsSubscribersTable,
-            NexusBankAccountsTable,
-            NexusBankTransactionsTable,
-            TalerIncomingPaymentsTable,
-            TalerRequestedPaymentsTable,
-            TalerInvalidIncomingPaymentsTable,
-            NexusBankConnectionsTable,
-            NexusBankMessagesTable,
-            NexusBankBalancesTable,
-            FacadesTable,
-            FacadeStateTable,
-            NexusScheduledTasksTable,
-            OfferedBankAccountsTable,
-            NexusPermissionsTable,
-            AnastasisIncomingPaymentsTable
-        )
-    }
-}
-
-fun dbCreateTables(connStringFromEnv: String) {
-    connectWithSchema(getJdbcConnectionFromPg(connStringFromEnv))
-    if (isPostgres()) {
-        execCommand(listOf(
-            "libeufin-load-sql",
-            "-d",
-            connStringFromEnv,
-            "-s",
-            "nexus"
-        ))
-        return
-    }
-    // Still using the legacy way for other DBMSs, like SQLite.
-    transaction {
-        SchemaUtils.create(
-            XLibeufinBankUsersTable,
-            NexusScheduledTasksTable,
-            NexusUsersTable,
-            PaymentInitiationsTable,
-            NexusEbicsSubscribersTable,
-            NexusBankAccountsTable,
-            NexusBankBalancesTable,
-            NexusBankTransactionsTable,
-            AnastasisIncomingPaymentsTable,
-            TalerIncomingPaymentsTable,
-            TalerRequestedPaymentsTable,
-            FacadeStateTable,
-            TalerInvalidIncomingPaymentsTable,
-            NexusBankConnectionsTable,
-            NexusBankMessagesTable,
-            FacadesTable,
-            OfferedBankAccountsTable,
-            NexusPermissionsTable
-        )
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB_helpers.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB_helpers.kt
deleted file mode 100644
index b39a05f2..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB_helpers.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-package tech.libeufin.nexus
-
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.node.ObjectNode
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.http.*
-import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.server.GetTransactionsParams
-import tech.libeufin.nexus.server.Pain001Data
-import tech.libeufin.util.notFound
-import java.time.Instant
-
-fun getBankAccount(label: String): NexusBankAccountEntity {
-    val maybeBankAccount = transaction {
-        NexusBankAccountEntity.findByName(label)
-    }
-    return maybeBankAccount ?:
-    throw NexusError(
-        HttpStatusCode.NotFound,
-        "Account $label not found"
-    )
-}
-
-/**
- * Queries the database according to the GET /transactions
- * parameters.
- */
-fun getIngestedTransactions(params: GetTransactionsParams): List<JsonNode> =
-    transaction {
-        val bankAccount = getBankAccount(params.bankAccountId)
-        val maybeResult = NexusBankTransactionEntity.find {
-            NexusBankTransactionsTable.bankAccount eq bankAccount.id.value and 
(
-                    NexusBankTransactionsTable.id greaterEq params.startIndex
-                    )
-        }.sortedBy { it.id.value }.take(params.resultSize.toInt()) // Smallest 
index (= earliest transaction) first
-        // Converting the result to the HTTP response type.
-        maybeResult.map {
-            val element: ObjectNode = jacksonObjectMapper().createObjectNode()
-            element.put("index", it.id.value.toString())
-            val txObj: JsonNode = 
jacksonObjectMapper().readTree(it.transactionJson)
-            element.set<JsonNode>("camtData", txObj)
-            return@map element
-        }
-    }
-
-// Gets connection or throws.
-fun getBankConnection(connId: String): NexusBankConnectionEntity {
-    val maybeConn = transaction {
-        NexusBankConnectionEntity.find {
-            NexusBankConnectionsTable.connectionId eq connId
-        }.firstOrNull()
-    }
-    if (maybeConn == null) throw notFound("Bank connection $connId not found")
-    return maybeConn
-}
-
-/**
- * Retrieve payment initiation from database, raising exception if not found.
- */
-fun getPaymentInitiation(uuid: Long): PaymentInitiationEntity {
-    return transaction {
-        PaymentInitiationEntity.findById(uuid)
-    } ?: throw NexusError(
-        HttpStatusCode.NotFound,
-        "Payment '$uuid' not found"
-    )
-}
-
-/**
- * Gets a prepared payment starting from its 'payment information id'.
- * Note: although the terminology comes from CaMt, a 'payment information id'
- * is indeed any UID that identifies the payment.  For this reason, also
- * the x-libeufin-bank logic uses this helper.
- *
- * Returns the prepared payment, or null if that's not found.  Not throwing
- * any exception because the null case is common: not every transaction being
- * processed by Neuxs was prepared/initiated here; incoming transactions are
- * one example.
- */
-fun getPaymentInitiation(pmtInfId: String): PaymentInitiationEntity? =
-    transaction {
-        PaymentInitiationEntity.find(
-            PaymentInitiationsTable.paymentInformationId.eq(pmtInfId)
-        ).firstOrNull()
-    }
-
-/**
- * Insert one row in the database, and leaves it marked as non-submitted.
- * @param debtorAccount the mnemonic id assigned by the bank to one bank
- * account of the subscriber that is creating the pain entity.  In this case,
- * it will be the account whose money will pay the wire transfer being defined
- * by this pain document.
- */
-fun addPaymentInitiation(
-    paymentData: Pain001Data,
-    debtorAccount: NexusBankAccountEntity
-): PaymentInitiationEntity {
-    return transaction {
-
-        val now = Instant.now().toEpochMilli()
-        val nowHex = now.toString(16)
-        val painCounter = debtorAccount.pain001Counter++
-        val painHex = painCounter.toString(16)
-        val acctHex = debtorAccount.id.value.toString(16)
-
-        PaymentInitiationEntity.new {
-            currency = paymentData.currency
-            bankAccount = debtorAccount
-            subject = paymentData.subject
-            sum = paymentData.sum
-            creditorName = paymentData.creditorName
-            creditorBic = paymentData.creditorBic
-            creditorIban = paymentData.creditorIban
-            preparationDate = now
-            endToEndId = paymentData.endToEndId ?: 
"leuf-e-$nowHex-$painHex-$acctHex"
-            messageId = "leuf-mp1-$nowHex-$painHex-$acctHex"
-            paymentInformationId = "leuf-p-$nowHex-$painHex-$acctHex"
-            instructionId = "leuf-i-$nowHex-$painHex-$acctHex"
-        }
-    }
-}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt
deleted file mode 100644
index 0cc340e8..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt
+++ /dev/null
@@ -1,35 +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.nexus
-
-import io.ktor.http.HttpStatusCode
-import tech.libeufin.util.LibeufinErrorCode
-
-data class NexusError(
-    val statusCode: HttpStatusCode,
-    val reason: String,
-    val code: LibeufinErrorCode? = null
-    ) :
-    Exception("$reason (HTTP status $statusCode)")
-
-fun NexusAssert(condition: Boolean, errorMsg: String): Boolean {
-    if (! condition) throw NexusError(HttpStatusCode.InternalServerError, 
errorMsg)
-    return true
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt
deleted file mode 100644
index 76234fbe..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-package tech.libeufin.nexus
-
-import CamtBankAccountEntry
-import EntryStatus
-import TransactionDetails
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.http.*
-import org.jetbrains.exposed.dao.flushCache
-import org.jetbrains.exposed.sql.SortOrder
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.TransactionManager
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.server.NexusFacadeType
-
-fun getFacadeState(fcid: String): FacadeStateEntity {
-    return transaction {
-        val facade = FacadeEntity.find {
-            FacadesTable.facadeName eq fcid
-        }.firstOrNull() ?: throw NexusError(
-            HttpStatusCode.NotFound,
-            "Could not find facade '${fcid}'"
-        )
-        FacadeStateEntity.find {
-            FacadeStateTable.facade eq facade.id.value
-        }.firstOrNull() ?: throw NexusError(
-            HttpStatusCode.NotFound,
-            "Could not find any state for facade: $fcid"
-        )
-    }
-}
-
-fun getFacadeBankAccount(fcid: String): NexusBankAccountEntity {
-    return transaction {
-        val facadeState = getFacadeState(fcid)
-        NexusBankAccountEntity.findByName(facadeState.bankAccount) ?: throw 
NexusError(
-            HttpStatusCode.NotFound,
-            "The facade: $fcid doesn't manage bank account: 
${facadeState.bankAccount}"
-        )
-    }
-}
-
-/**
- * Ingests transactions for those facades accounting for bankAccountId.
- * 'incomingFilterCb' decides whether the facade accepts the payment;
- * if not, refundCb prepares a refund.  The 'txStatus' parameter decides
- * at which state one transaction deserve to fuel Taler transactions. BOOK
- * is conservative, and with some banks the delay can be significant.  PNDG
- * instead reacts faster, but risks that one transaction gets undone by the
- * bank and never reach the BOOK state; this would mean a loss and/or admin
- * burden.
- */
-fun ingestFacadeTransactions(
-    bankAccountId: String,
-    facadeType: NexusFacadeType,
-    incomingFilterCb: ((NexusBankTransactionEntity, TransactionDetails) -> 
Unit)?,
-    refundCb: ((NexusBankAccountEntity, Long) -> Unit)?,
-    txStatus: EntryStatus = EntryStatus.BOOK
-) {
-    fun ingest(bankAccount: NexusBankAccountEntity, facade: FacadeEntity) {
-        logger.debug(
-            "Ingesting transactions for $facadeType facade 
${facade.id.value}," +
-                    " and bank account: ${bankAccount.bankAccountName}"
-        )
-        val facadeState = getFacadeState(facade.facadeName)
-        var lastId = facadeState.highestSeenMessageSerialId
-        NexusBankTransactionEntity.find {
-            /** Those with "our" bank account involved */
-            NexusBankTransactionsTable.bankAccount eq bankAccount.id.value and
-                    /** Those that are booked */
-                    (NexusBankTransactionsTable.status eq txStatus) and
-                    /** Those that came later than the latest processed 
payment */
-                    (NexusBankTransactionsTable.id.greater(lastId))
-        }.orderBy(Pair(NexusBankTransactionsTable.id, SortOrder.ASC)).forEach {
-            // Incoming payment.
-            val tx = jacksonObjectMapper().readValue(
-                it.transactionJson,
-                CamtBankAccountEntry::class.java
-            )
-            /**
-             * Need transformer from "JSON tx" to TransactionDetails?.
-             */
-            val details: TransactionDetails? = 
tx.batches?.get(0)?.batchTransactions?.get(0)?.details
-            if (details == null) {
-                logger.warn("A void money movement (${tx.accountServicerRef}) 
made it through the ingestion: VERY strange")
-                return@forEach
-            }
-            when (tx.creditDebitIndicator) {
-                CreditDebitIndicator.CRDT -> {
-                    if (incomingFilterCb != null) {
-                        incomingFilterCb(
-                            it, // payment DB object
-                            details // wire transfer details
-                        )
-                    }
-                }
-                else -> Unit
-            }
-            lastId = it.id.value
-        }
-        try {
-            if (refundCb != null) {
-                refundCb(
-                    bankAccount,
-                    facadeState.highestSeenMessageSerialId
-                )
-            }
-        } catch (e: Exception) {
-            logger.warn("Sending refund payment failed: ${e.message}")
-        }
-        facadeState.highestSeenMessageSerialId = lastId
-    }
-    // invoke ingestion for all the facades
-    transaction {
-        FacadeEntity.find { FacadesTable.type eq facadeType.facadeType 
}.forEach {
-            val facadeBankAccount = getFacadeBankAccount(it.facadeName)
-            if (facadeBankAccount.bankAccountName == bankAccountId)
-                ingest(facadeBankAccount, it)
-            flushCache()
-        }
-    }
-}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JsonLiterals.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/JsonLiterals.kt
deleted file mode 100644
index cd89e011..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/JsonLiterals.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2021 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.nexus
-
-import com.fasterxml.jackson.databind.node.ObjectNode
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-
-class JsonObjectMaker(val obj: ObjectNode) {
-    fun prop(key: String, value: String?) {
-        obj.put(key, value)
-    }
-    fun prop(key: String, value: Long?) {
-        obj.put(key, value)
-    }
-    fun prop(key: String, value: Int?) {
-        obj.put(key, value)
-    }
-}
-
-fun makeJsonObject(f: JsonObjectMaker.() -> Unit): ObjectNode {
-    val mapper = jacksonObjectMapper()
-    val obj = mapper.createObjectNode()
-    f(JsonObjectMaker(obj))
-    return obj
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
deleted file mode 100644
index 5b453530..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ /dev/null
@@ -1,200 +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.nexus
-
-import com.github.ajalt.clikt.output.CliktHelpFormatter
-import com.github.ajalt.clikt.parameters.arguments.argument
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import tech.libeufin.util.CryptoUtil.hashpw
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import com.github.ajalt.clikt.parameters.types.int
-import execThrowableOrTerminate
-import com.github.ajalt.clikt.core.*
-import com.github.ajalt.clikt.parameters.options.*
-import io.ktor.server.application.*
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import startServer
-import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData
-import tech.libeufin.nexus.iso20022.createPain001document
-import tech.libeufin.nexus.iso20022.parseCamtMessage
-import tech.libeufin.nexus.server.EbicsDialects
-import tech.libeufin.nexus.server.nexusApp
-import tech.libeufin.util.*
-import java.io.File
-import kotlin.system.exitProcess
-
-val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
-const val NEXUS_DB_ENV_VAR_NAME = "LIBEUFIN_NEXUS_DB_CONNECTION"
-
-class NexusCommand : CliktCommand() {
-    init { versionOption(getVersion()) }
-    override fun run() = Unit
-}
-
-class Serve : CliktCommand("Run nexus HTTP server") {
-    init {
-        context {
-            helpFormatter = CliktHelpFormatter(showDefaultValues = 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)
-    // Prevent IPv6 mode:
-    // private val host by option().default("127.0.0.1")
-    private val port by option().int().default(5001)
-    private val withUnixSocket by option(
-        help = "Bind the Sandbox to the Unix domain socket at PATH.  
Overrides" +
-                " --port, when both are given", metavar = "PATH"
-    )
-    private val logLevel by option()
-    override fun run() {
-        setLogLevel(logLevel)
-        execThrowableOrTerminate { 
dbCreateTables(getDbConnFromEnv(NEXUS_DB_ENV_VAR_NAME)) }
-        CoroutineScope(Dispatchers.IO).launch(fallback) { 
whileTrueOperationScheduler() }
-        if (withUnixSocket != null) {
-            startServer(
-                withUnixSocket!!,
-                app = nexusApp
-            )
-            exitProcess(0)
-        }
-        logger.info("Starting Nexus on port ${this.port}")
-        startServerWithIPv4Fallback(
-            options = StartServerOptions(
-                ipv4OnlyOpt = this.ipv4Only,
-                localhostOnlyOpt = this.localhostOnly,
-                portOpt = this.port
-            ),
-            app = nexusApp
-        )
-    }
-}
-
-/**
- * This command purpose is to let the user then _manually_
- * tune the pain.001, to upload it to online verifiers.
- */
-class GenPain : CliktCommand(
-    "Generate random pain.001 document for 'pf' dialect, printing to STDOUT."
-) {
-    private val logLevel by option(
-        help = "Set the log level to: 'off', 'error', 'warn', 'info', 'debug', 
'trace', 'all'"
-    )
-    private val dialect by option(
-        help = "EBICS dialect using the pain.001 being generated.  Defaults to 
'pf' (PostFinance)",
-    ).default("pf")
-    override fun run() {
-        setLogLevel(logLevel)
-        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 = this.dialect
-        )
-        println(pain001)
-    }
-}
-class ParseCamt : CliktCommand("Parse camt.05x file, outputs JSON in libEufin 
internal representation.") {
-    private val logLevel by option(
-        help = "Set the log level to: 'off', 'error', 'warn', 'info', 'debug', 
'trace', 'all'"
-    )
-    private val withPfDialect by option(
-        help = "Set the dialect to 'pf' (PostFinance).  If not given, it 
defaults to GLS."
-    ).flag(default = false)
-    private val filename by argument("FILENAME", "File in CAMT format")
-    override fun run() {
-        setLogLevel(logLevel)
-        val camtText = File(filename).readText(Charsets.UTF_8)
-        val dialect = if (withPfDialect) EbicsDialects.POSTFINANCE.dialectName 
else null
-        val res = parseCamtMessage(XMLUtil.parseStringIntoDom(camtText), 
dialect)
-        
println(jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(res))
-    }
-}
-
-class ResetTables : CliktCommand("Drop all the tables from the database") {
-    init {
-        context {
-            helpFormatter = CliktHelpFormatter(showDefaultValues = true)
-        }
-    }
-    override fun run() {
-        val dbConnString = getDbConnFromEnv(NEXUS_DB_ENV_VAR_NAME)
-        execThrowableOrTerminate {
-            dbDropTables(dbConnString)
-            dbCreateTables(dbConnString)
-        }
-    }
-}
-
-class Superuser : CliktCommand("Add superuser or change pw") {
-    private val username by argument("USERNAME", "User name of superuser")
-    private val password by option().prompt(requireConfirmation = true, 
hideInput = true)
-    override fun run() {
-        execThrowableOrTerminate {
-            dbCreateTables(getDbConnFromEnv(NEXUS_DB_ENV_VAR_NAME))
-        }
-        transaction {
-            val hashedPw = hashpw(password)
-            val user = NexusUserEntity.find { NexusUsersTable.username eq 
username }.firstOrNull()
-            if (user == null) {
-                NexusUserEntity.new {
-                    this.username = this@Superuser.username
-                    this.passwordHash = hashedPw
-                    this.superuser = true
-                }
-            } else {
-                if (!user.superuser) {
-                    System.err.println("Can only change password for superuser 
with this command.")
-                    throw ProgramResult(1)
-                }
-                user.passwordHash = hashedPw
-            }
-        }
-    }
-}
-
-fun main(args: Array<String>) {
-    NexusCommand()
-        .subcommands(Serve(), Superuser(), ParseCamt(), ResetTables(), 
GenPain())
-        .main(args)
-}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt
deleted file mode 100644
index 2e67eb3b..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt
+++ /dev/null
@@ -1,172 +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.nexus
-
-import com.cronutils.model.definition.CronDefinitionBuilder
-import com.cronutils.model.time.ExecutionTime
-import com.cronutils.parser.CronParser
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.client.HttpClient
-import kotlinx.coroutines.*
-import kotlinx.coroutines.time.delay
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
-import tech.libeufin.nexus.bankaccount.submitAllPaymentInitiations
-import tech.libeufin.nexus.server.FetchSpecJson
-import tech.libeufin.nexus.server.client
-import java.lang.IllegalArgumentException
-import java.time.Duration
-import java.time.Instant
-import java.time.ZonedDateTime
-import kotlin.system.exitProcess
-
-private data class TaskSchedule(
-    val taskId: Long,
-    val name: String,
-    val type: String,
-    val resourceType: String,
-    val resourceId: String,
-    val params: String
-)
-
-private suspend fun runTask(client: HttpClient, sched: TaskSchedule) {
-    logger.info("running task $sched")
-    try {
-        when (sched.resourceType) {
-            "bank-account" -> {
-                when (sched.type) {
-                    // Downloads and ingests the payment records from the bank.
-                    "fetch" -> {
-                        val fetchSpec = 
jacksonObjectMapper().readValue(sched.params, FetchSpecJson::class.java)
-                        fetchBankAccountTransactions(client, fetchSpec, 
sched.resourceId)
-                        /**
-                         * NOTE: the previous operation COULD have had 
problems but that
-                         * is tolerated because the communication with the 
backend CAN be
-                         * unreliable.  As of logging: not doing it here 
twice, since every
-                         * error should already have been logged when it 
originated.
-                         */
-                    }
-                    // Submits the payment preparations that are found in the 
database.
-                    "submit" -> {
-                        submitAllPaymentInitiations(client, sched.resourceId)
-                    }
-                    else -> {
-                        logger.error("task type ${sched.type} not supported")
-                    }
-                }
-            }
-            else -> logger.error("task on resource ${sched.resourceType} not 
supported")
-        }
-    }
-    catch (e: Exception) {
-        logger.error("Exception during task $sched: ${e.message})")
-        /**
-         *  Not exiting the process since the error can be temporary:
-         *  name resolution problem, Nexus connectivity problem, ...
-         */
-    }
-    catch (so: StackOverflowError) {
-        logger.error(so.stackTraceToString())
-        exitProcess(1)
-    }
-}
-
-object NexusCron {
-    val parser = run {
-        val cronDefinition =
-            CronDefinitionBuilder.defineCron()
-                .withSeconds().and()
-                .withMinutes().and()
-                .withHours().and()
-                .withDayOfMonth().optional().and()
-                .withMonth().optional().and()
-                .withDayOfWeek().optional()
-                .and().instance()
-        CronParser(cronDefinition)
-    }
-}
-
-// Fails whenever an unmanaged Throwable reaches the root coroutine.
-val fallback = CoroutineExceptionHandler { _, err ->
-    logger.error(err.stackTraceToString())
-    exitProcess(1)
-}
-
-// Internal routine ultimately scheduling the tasks.
-private suspend fun operationScheduler(httpClient: HttpClient) {
-    // First, assign next execution time stamps to all tasks that need them
-    transaction {
-        NexusScheduledTaskEntity.find {
-            NexusScheduledTasksTable.nextScheduledExecutionSec.isNull()
-        }.forEach {
-            val cron = try { NexusCron.parser.parse(it.taskCronspec) }
-            catch (e: IllegalArgumentException) {
-                logger.error("invalid cronspec in schedule 
${it.resourceType}/${it.resourceId}/${it.taskName}")
-                return@forEach
-            }
-            val zonedNow = ZonedDateTime.now()
-            val parsedCron = ExecutionTime.forCron(cron)
-            val next = parsedCron.nextExecution(zonedNow)
-            logger.info("Scheduling task ${it.taskName} at $next (now is 
$zonedNow).")
-            it.nextScheduledExecutionSec = next.get().toEpochSecond()
-        }
-    }
-    val nowSec = Instant.now().epochSecond
-    // Second, find tasks that are due
-    val dueTasks = transaction {
-        NexusScheduledTaskEntity.find {
-            NexusScheduledTasksTable.nextScheduledExecutionSec lessEq nowSec
-        }.map {
-            TaskSchedule(it.id.value, it.taskName, it.taskType, 
it.resourceType, it.resourceId, it.taskParams)
-        }
-    } // Execute those due tasks and reset to null the next execution time.
-    dueTasks.forEach {
-        runTask(httpClient, it)
-        transaction {
-            val t = NexusScheduledTaskEntity.findById(it.taskId)
-            if (t != null) {
-                // Reset next scheduled execution
-                t.nextScheduledExecutionSec = null
-                t.prevScheduledExecutionSec = nowSec
-            }
-        }
-    }
-
-}
-
-// Alternative scheduler based on Java Timer, but same perf. as the while-true 
one.
-/*
-private val javaTimer = Timer()
-suspend fun javaTimerOperationScheduler(httpClient: HttpClient) {
-    operationScheduler(httpClient)
-    javaTimer.schedule(
-        delay = 1000L,
-        action =  { runBlocking { javaTimerOperationScheduler(httpClient) } }
-    )
-}
-*/
-
-suspend fun whileTrueOperationScheduler(httpClient: HttpClient = client) {
-    while (true) {
-        operationScheduler(httpClient)
-        // Wait the shortest period that the cron spec would allow.
-        delay(Duration.ofSeconds(1))
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
deleted file mode 100644
index 19113985..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
+++ /dev/null
@@ -1,710 +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.nexus
-
-import CamtBankAccountEntry
-import TransactionDetails
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.server.application.ApplicationCall
-import io.ktor.server.application.call
-import io.ktor.client.*
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.content.TextContent
-import io.ktor.http.*
-import io.ktor.server.request.receive
-import io.ktor.server.response.*
-import io.ktor.server.routing.Route
-import io.ktor.server.routing.get
-import io.ktor.server.routing.post
-import io.ktor.server.util.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.coroutineScope
-import org.jetbrains.exposed.dao.Entity
-import org.jetbrains.exposed.dao.id.IdTable
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.transactions.TransactionManager
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.server.*
-import tech.libeufin.nexus.xlibeufinbank.ingestXLibeufinBankMessage
-import tech.libeufin.util.*
-import java.net.URL
-import kotlin.math.abs
-import kotlin.math.min
-
-/**
- * Request body for "$TWG_BASE_URL/transfer".
- */
-data class TalerTransferRequest(
-    val request_uid: String,
-    val amount: String,
-    val exchange_base_url: String,
-    val wtid: String,
-    val credit_account: String // payto://-format
-)
-
-data class TalerTransferResponse(
-    /**
-     * Point in time when Nexus put the payment instruction into the database.
-     */
-    val timestamp: GnunetTimestamp,
-    val row_id: Long
-)
-
-/**
- * History accounting data structures, typically
- * used to build JSON responses.
- */
-data class TalerIncomingBankTransaction(
-    val row_id: Long,
-    val date: GnunetTimestamp, // timestamp
-    val amount: String,
-    val debit_account: String,
-    val reserve_pub: String
-)
-
-data class TalerIncomingHistory(
-    var incoming_transactions: MutableList<TalerIncomingBankTransaction> = 
mutableListOf(),
-    val credit_account: String
-)
-
-data class TalerOutgoingBankTransaction(
-    val row_id: Long,
-    val date: GnunetTimestamp, // timestamp
-    val amount: String,
-    val credit_account: String, // payto form,
-    val debit_account: String,
-    val wtid: String,
-    val exchange_base_url: String
-)
-
-data class TalerOutgoingHistory(
-    var outgoing_transactions: MutableList<TalerOutgoingBankTransaction> = 
mutableListOf()
-)
-
-data class GnunetTimestamp(val t_s: Long)
-
-/**
- * Sort query results in descending order for negative deltas, and ascending 
otherwise.
- */
-fun <T : Entity<Long>> SizedIterable<T>.orderTaler(delta: Int): List<T> {
-    return if (delta < 0) {
-        this.sortedByDescending { it.id }
-    } else {
-        this.sortedBy { it.id }
-    }
-}
-
-/** Builds the comparison operator for history entries based on the sign of 
'delta'  */
-fun getComparisonOperator(delta: Int, start: Long, table: IdTable<Long>): 
Op<Boolean> {
-    return if (delta < 0) {
-        Expression.build {
-            table.id less start
-        }
-    } else {
-        Expression.build {
-            table.id greater start
-        }
-    }
-}
-
-fun expectLong(param: String?, allowNegative: Boolean = false): Long {
-    if (param == null) throw badRequest("'$param' is not Long")
-    val maybeLong = try { param.toLong() } catch (e: Exception) {
-        throw badRequest("'$param' is not Long")
-    }
-    if (!allowNegative && maybeLong < 0)
-        throw badRequest("Not expecting a negative: $param")
-    return maybeLong
-}
-
-// Helper handling 'start' being optional and its dependence on 'delta'.
-fun handleStartArgument(start: String?, delta: Int): Long {
-    if (start == null) {
-        if (delta >= 0) return -1
-        return Long.MAX_VALUE
-    }
-    return expectLong(start)
-}
-
-/**
- * The Taler layer cannot rely on the ktor-internal JSON-converter/responder,
- * because this one adds a "charset" extra information in the Content-Type 
header
- * that makes the GNUnet JSON parser unhappy.
- *
- * The workaround is to explicitly convert the 'data class'-object into a JSON
- * string (what this function does), and use the simpler respondText method.
- */
-fun customConverter(body: Any): String {
-    return jacksonObjectMapper().writeValueAsString(body)
-}
-
-// Handle a Taler Wire Gateway /transfer request.
-private suspend fun talerTransfer(call: ApplicationCall) {
-    val transferRequest = call.receive<TalerTransferRequest>()
-    val amountObj = parseAmount(transferRequest.amount)
-    // FIXME: Right now we only parse the credit_account, should we also 
validate that it matches our account info?
-    // FIXME, another parse happens below; is this really useful here?
-    parsePayto(transferRequest.credit_account)
-    val facadeId = expectNonNull(call.parameters["fcid"])
-    val opaqueRowId = transaction {
-        call.request.requirePermission(PermissionQuery("facade", facadeId, 
"facade.talerwiregateway.transfer"))
-        val facade = FacadeEntity.find { FacadesTable.facadeName eq facadeId 
}.firstOrNull() ?: throw NexusError(
-            HttpStatusCode.NotFound,
-            "Could not find facade '${facadeId}'"
-        )
-        val creditorData = parsePayto(transferRequest.credit_account)
-        /** Checking the UID has the desired characteristics */
-        TalerRequestedPaymentEntity.find {
-            TalerRequestedPaymentsTable.requestUid eq 
transferRequest.request_uid
-        }.forEach {
-            if (
-                (it.amount != transferRequest.amount) or
-                (it.creditAccount != transferRequest.exchange_base_url) or
-                (it.wtid != transferRequest.wtid)
-            ) {
-                throw NexusError(
-                    HttpStatusCode.Conflict,
-                    "This uid (${transferRequest.request_uid}) belongs to a 
different payment already"
-                )
-            }
-        }
-        val exchangeBankAccount = getFacadeBankAccount(facadeId)
-        val paymentSubject = "${transferRequest.wtid} 
${transferRequest.exchange_base_url}"
-        val pain001 = addPaymentInitiation(
-            Pain001Data(
-                creditorIban = creditorData.iban,
-                creditorBic = creditorData.bic,
-                creditorName = creditorData.receiverName ?: throw NexusError(
-                    HttpStatusCode.BadRequest, "Payto did not mention account 
owner"
-                ),
-                subject = paymentSubject,
-                sum = amountObj.amount,
-                currency = amountObj.currency
-            ),
-            exchangeBankAccount
-        )
-        logger.debug("Taler requests payment: ${transferRequest.wtid}")
-        val row = TalerRequestedPaymentEntity.new {
-            this.facade = facade
-            preparedPayment = pain001
-            exchangeBaseUrl = transferRequest.exchange_base_url
-            requestUid = transferRequest.request_uid
-            amount = transferRequest.amount
-            wtid = transferRequest.wtid
-            creditAccount = transferRequest.credit_account
-        }
-        row.id.value
-    }
-    return call.respond(
-        TextContent(
-            customConverter(
-                TalerTransferResponse(
-                    /**
-                     * Normally should point to the next round where the 
background
-                     * routine will send new PAIN.001 data to the bank; work 
in progress..
-                     */
-                    timestamp = GnunetTimestamp(System.currentTimeMillis() / 
1000L),
-                    row_id = opaqueRowId
-                )
-            ),
-            ContentType.Application.Json
-        )
-    )
-}
-
-// Processes new transactions and stores TWG-specific data in
-fun talerFilter(
-    payment: NexusBankTransactionEntity,
-    txDtls: TransactionDetails
-) {
-    var isInvalid = false // True when pub is invalid or duplicate.
-    val subject = txDtls.unstructuredRemittanceInformation ?: throw
-            internalServerError("Payment '${payment.accountTransactionId}' has 
no subject, can't extract reserve pub.")
-    val debtorName = txDtls.debtor?.name
-    if (debtorName == null) {
-        logger.warn("empty debtor name")
-        return
-    }
-    val debtorAcct = txDtls.debtorAccount
-    if (debtorAcct == null) {
-        // FIXME: Report payment, we can't even send it back
-        logger.warn("empty debtor account")
-        return
-    }
-    val debtorIban = debtorAcct.iban
-    if (debtorIban == null) {
-        // FIXME: Report payment, we can't even send it back
-        logger.warn("non-iban debtor account")
-        return
-    }
-    val debtorBic = txDtls.debtorAgent?.bic
-    if (debtorBic == null) {
-        logger.warn("Not allowing transactions missing the BIC.  IBAN and 
name: ${debtorIban}, $debtorName")
-        return
-    }
-    val reservePub = extractReservePubFromSubject(subject)
-    if (reservePub == null) {
-        logger.warn("could not find reserve pub in remittance information")
-        TalerInvalidIncomingPaymentEntity.new {
-            this.payment = payment
-            timestampMs = System.currentTimeMillis()
-        }
-        // Will be paid back by the refund handler.
-        return
-    }
-    // Check if reserve_pub was used already
-    val maybeExist = TalerIncomingPaymentEntity.find {
-        TalerIncomingPaymentsTable.reservePublicKey eq reservePub
-    }.firstOrNull()
-    if (maybeExist != null) {
-        val msg = "Reserve pub '$reservePub' was used already"
-        logger.info(msg)
-        isInvalid = true
-    }
-
-    if (!CryptoUtil.checkValidEddsaPublicKey(reservePub)) {
-        logger.info("invalid public key detected")
-        isInvalid = true
-    }
-    if (isInvalid) {
-        TalerInvalidIncomingPaymentEntity.new {
-            this.payment = payment
-            timestampMs = System.currentTimeMillis()
-        }
-        // Will be paid back by the refund handler.
-        return
-    }
-    TalerIncomingPaymentEntity.new {
-        this.payment = payment
-        reservePublicKey = reservePub
-        timestampMs = System.currentTimeMillis()
-        debtorPaytoUri = buildIbanPaytoUri(
-            debtorIban,
-            debtorBic,
-            debtorName
-        )
-    }
-    val dbTx = TransactionManager.currentOrNull() ?: throw NexusError(
-        HttpStatusCode.InternalServerError,
-        "talerFilter(): unexpected execution out of a DB transaction"
-    )
-    // Only supporting Postgres' NOTIFY.
-    if (dbTx.isPostgres()) {
-        val channelName = buildChannelName(
-            NotificationsChannelDomains.LIBEUFIN_TALER_INCOMING,
-            payment.bankAccount.iban
-        )
-        logger.debug("NOTIFYing on domain" +
-                " ${NotificationsChannelDomains.LIBEUFIN_TALER_INCOMING}" +
-                " for IBAN: ${payment.bankAccount.iban}.  Resulting channel" +
-                " name: $channelName.")
-        dbTx.postgresNotify(channelName)
-    }
-}
-
-fun maybeTalerRefunds(bankAccount: NexusBankAccountEntity, lastSeenId: Long) {
-    logger.debug(
-        "Searching refundable payments of account: 
${bankAccount.bankAccountName}," +
-                " after last seen transaction id: $lastSeenId"
-    )
-    transaction {
-        TalerInvalidIncomingPaymentsTable.innerJoin(
-            NexusBankTransactionsTable,
-            { NexusBankTransactionsTable.id },
-            { TalerInvalidIncomingPaymentsTable.payment }
-        ).select {
-            /**
-             * Finds Taler-invalid incoming payments that weren't refunded
-             * yet and are newer than those processed along the last round.
-             */
-            TalerInvalidIncomingPaymentsTable.refunded eq false and
-                    (NexusBankTransactionsTable.bankAccount eq 
bankAccount.id.value) and
-                    (NexusBankTransactionsTable.id greater lastSeenId)
-        }.forEach {
-            // For each of them, extracts the wire details to reuse in the 
refund.
-            val paymentData = jacksonObjectMapper().readValue(
-                it[NexusBankTransactionsTable.transactionJson],
-                CamtBankAccountEntry::class.java
-            )
-            val batches = paymentData.batches
-            if (batches == null) {
-                logger.error(
-                    "Empty wire details encountered in transaction with" +
-                            " AcctSvcrRef: ${paymentData.accountServicerRef}." 
+
-                            " Taler can't refund."
-                )
-                throw NexusError(
-                    HttpStatusCode.InternalServerError,
-                    "Unexpected void payment, cannot refund"
-                )
-            }
-            val debtorIban = 
batches[0].batchTransactions[0].details.debtorAccount?.iban
-            if (debtorIban == null) {
-                logger.error("Could not find a IBAN to refund in transaction 
(AcctSvcrRef): ${paymentData.accountServicerRef}, aborting refund")
-                throw NexusError(HttpStatusCode.InternalServerError, "IBAN to 
refund not found")
-            }
-            val debtorAgent = 
batches[0].batchTransactions[0].details.debtorAgent
-            if (debtorAgent?.bic == null) {
-                logger.error("Could not find the BIC of refundable IBAN at 
transaction (AcctSvcrRef): ${paymentData.accountServicerRef}, aborting refund")
-                throw NexusError(HttpStatusCode.InternalServerError, "BIC to 
refund not found")
-            }
-            val debtorName = 
batches[0].batchTransactions[0].details.debtor?.name
-            if (debtorName == null) {
-                logger.error("Could not find the owner's name of refundable 
IBAN at transaction (AcctSvcrRef): ${paymentData.accountServicerRef}, aborting 
refund")
-                throw NexusError(HttpStatusCode.InternalServerError, "Name to 
refund not found")
-            }
-            // FIXME: investigate this amount!
-            val amount = batches[0].batchTransactions[0].amount
-            NexusAssert(
-                it[NexusBankTransactionsTable.creditDebitIndicator] == "CRDT" 
&&
-                        it[NexusBankTransactionsTable.bankAccount] == 
bankAccount.id,
-                "Cannot refund an _outgoing_ payment!"
-            )
-
-            // FIXME #7116
-            addPaymentInitiation(
-                Pain001Data(
-                    creditorIban = debtorIban,
-                    creditorBic = debtorAgent.bic,
-                    creditorName = debtorName,
-                    subject = "Taler refund of: 
${batches[0].batchTransactions[0].details.unstructuredRemittanceInformation}",
-                    sum = amount.value,
-                    currency = amount.currency
-                ),
-                bankAccount // the Exchange bank account.
-            )
-            logger.debug("Refund of transaction (AcctSvcrRef): 
${paymentData.accountServicerRef} got prepared")
-            it[TalerInvalidIncomingPaymentsTable.refunded] = true
-        }
-    }
-}
-
-/**
- * Handle a /taler/history/outgoing request.
- */
-private suspend fun historyOutgoing(call: ApplicationCall) {
-    val facadeId = expectNonNull(call.parameters["fcid"])
-    call.request.requirePermission(PermissionQuery("facade", facadeId, 
"facade.talerwiregateway.history"))
-    val param = call.expectUrlParameter("delta")
-    val delta: Int = try {
-        param.toInt()
-    } catch (e: Exception) {
-        throw EbicsProtocolError(HttpStatusCode.BadRequest, "'${param}' is not 
Int")
-    }
-    val start: Long = 
handleStartArgument(call.request.queryParameters["start"], delta)
-    val startCmpOp = getComparisonOperator(delta, start, 
TalerRequestedPaymentsTable)
-    /* retrieve database elements */
-    val history = TalerOutgoingHistory()
-    transaction {
-        /** Retrieve all the outgoing payments from the _clean Taler outgoing 
table_ */
-        val subscriberBankAccount = getFacadeBankAccount(facadeId)
-        val reqPayments = mutableListOf<TalerRequestedPaymentEntity>()
-        val reqPaymentsWithUnconfirmed = TalerRequestedPaymentEntity.find {
-            startCmpOp
-        }.orderTaler(delta)
-        reqPaymentsWithUnconfirmed.forEach {
-            if (it.preparedPayment.confirmationTransaction != null) {
-                reqPayments.add(it)
-            }
-        }
-        if (reqPayments.isNotEmpty()) {
-            reqPayments.subList(0, min(abs(delta), reqPayments.size)).forEach {
-                history.outgoing_transactions.add(
-                    TalerOutgoingBankTransaction(
-                        row_id = it.id.value,
-                        amount = it.amount,
-                        wtid = it.wtid,
-                        date = 
GnunetTimestamp(it.preparedPayment.preparationDate / 1000L),
-                        credit_account = it.creditAccount,
-                        debit_account = buildIbanPaytoUri(
-                            subscriberBankAccount.iban,
-                            subscriberBankAccount.bankCode,
-                            subscriberBankAccount.accountHolder,
-                        ),
-                        exchange_base_url = it.exchangeBaseUrl
-                    )
-                )
-            }
-        }
-    }
-    if (history.outgoing_transactions.size == 0) {
-        call.respond(HttpStatusCode.NoContent)
-        return
-    }
-    call.respond(
-        status = HttpStatusCode.OK,
-        TextContent(customConverter(history), ContentType.Application.Json)
-    )
-}
-
-// Handle a /taler-wire-gateway/history/incoming request.
-private suspend fun historyIncoming(call: ApplicationCall) {
-    val facadeId = expectNonNull(call.parameters["fcid"])
-    call.request.requirePermission(
-        PermissionQuery(
-            "facade",
-            facadeId,
-            "facade.talerwiregateway.history"
-        )
-    )
-    val longPollTimeoutPar = call.parameters["long_poll_ms"]
-    val longPollTimeout = if (longPollTimeoutPar != null) {
-        val longPollTimeoutValue = try { longPollTimeoutPar.toLong() }
-        catch (e: Exception) {
-            throw badRequest("long_poll_ms value is invalid")
-        }
-        longPollTimeoutValue
-    } else null
-    val param = call.expectUrlParameter("delta")
-    val delta: Int = try { param.toInt() } catch (e: Exception) {
-        throw EbicsProtocolError(HttpStatusCode.BadRequest, "'${param}' is not 
Int")
-    }
-    val start: Long = 
handleStartArgument(call.request.queryParameters["start"], delta)
-    val facadeBankAccount = getFacadeBankAccount(facadeId)
-    val startCmpOp = getComparisonOperator(delta, start, 
TalerIncomingPaymentsTable)
-    val listenHandle: PostgresListenHandle? = if (isPostgres() && 
longPollTimeout != null) {
-        val notificationChannelName = buildChannelName(
-            NotificationsChannelDomains.LIBEUFIN_TALER_INCOMING,
-            facadeBankAccount.iban
-        )
-        val handle = PostgresListenHandle(channelName = 
notificationChannelName)
-        handle.postgresListen()
-        handle
-    } else null
-
-    /**
-     * NOTE: the LISTEN command MAY also go inside this transaction,
-     * but LISTEN uses a connection other than the one provided by the
-     * transaction block.  More facts on the consequences are needed.
-     */
-    var result: List<TalerIncomingPaymentEntity> = transaction {
-        TalerIncomingPaymentEntity.find { startCmpOp }.orderTaler(delta)
-    }
-    // The request was lucky, unlisten then.
-    if (result.isNotEmpty() && listenHandle != null)
-        listenHandle.postgresUnlisten()
-
-    // The request was NOT lucky, wait now.
-    if (result.isEmpty() && listenHandle != null && longPollTimeout != null) {
-        logger.debug("Waiting for NOTIFY on channel 
${listenHandle.channelName}," +
-                " with timeout: $longPollTimeoutPar ms")
-        val notificationArrived = coroutineScope {
-            async(Dispatchers.IO) {
-                listenHandle.postgresGetNotifications(longPollTimeout)
-            }.await()
-        }
-        if (notificationArrived) {
-            /**
-             * NOTE: the query can still have zero results despite the
-             * notification.  That happens when the 'start' URI param is
-             * higher than the ID of the new row in the database.  Not
-             * an error.
-             */
-            result = transaction {
-                // addLogger(StdOutSqlLogger)
-                TalerIncomingPaymentEntity.find { startCmpOp 
}.orderTaler(delta)
-            }
-        }
-    }
-    /**
-     * Whether because of a timeout or a notification or of never slept, here 
it
-     * proceeds to the response (== resultOrWait.first IS EFFECTIVE).
-     */
-    val maybeNewPayments = result
-    val resp = if (maybeNewPayments.isNotEmpty()) {
-        val history = TalerIncomingHistory(
-            credit_account = buildIbanPaytoUri(
-                facadeBankAccount.iban,
-                facadeBankAccount.bankCode,
-                facadeBankAccount.accountHolder,
-            )
-        )
-        transaction {
-            maybeNewPayments.subList(
-                0,
-                min(abs(delta), maybeNewPayments.size)
-            ).forEach {
-                history.incoming_transactions.add(
-                    TalerIncomingBankTransaction(
-                        // Rounded timestamp
-                        date = GnunetTimestamp(it.timestampMs / 1000L),
-                        row_id = it.id.value,
-                        amount = "${it.payment.currency}:${it.payment.amount}",
-                        reserve_pub = it.reservePublicKey,
-                        debit_account = it.debtorPaytoUri
-                    )
-                )
-            }
-        }
-        history
-    } else null
-    if (resp == null) {
-        call.respond(HttpStatusCode.NoContent)
-        return
-    }
-    return call.respond(
-        status = HttpStatusCode.OK,
-        TextContent(customConverter(resp), ContentType.Application.Json)
-    )
-}
-
-/**
- * This call proxies /admin/add/incoming to the Sandbox,
- * which is the service keeping the transaction ledger.
- * The credentials are ASSUMED to be exchange/x (user/pass).
- *
- * In the future, a dedicated "add-incoming" facade should
- * be provided, offering the mean to store the credentials
- * at configuration time.
- */
-private suspend fun addIncoming(call: ApplicationCall) {
-    val facadeId = ensureNonNull(call.parameters["fcid"])
-    val currentBody = call.receive<String>()
-    val fromDb = transaction {
-        val f = FacadeEntity.findByName(facadeId) ?: throw notFound("facade 
$facadeId not found")
-        val facadeState = FacadeStateEntity.find {
-            FacadeStateTable.facade eq f.id
-        }.firstOrNull() ?: throw internalServerError("facade $facadeId has no 
state!")
-        val conn = 
NexusBankConnectionEntity.findByName(facadeState.bankConnection) ?: throw 
internalServerError(
-            "state of facade $facadeId has no bank connection!"
-        )
-        val sandboxUrl = 
URL(getConnectionPlugin(conn.type).getBankUrl(conn.connectionId))
-        // NOTE: the exchange username must be 'exchange', at the Sandbox.
-        return@transaction Pair(
-            url {
-                protocol = URLProtocol(sandboxUrl.protocol, 80)
-                host = sandboxUrl.host
-                if (sandboxUrl.port != 80)
-                    port = sandboxUrl.port
-                path(
-                    "demobanks",
-                    "default",
-                    "taler-wire-gateway",
-                    "exchange",
-                    "admin",
-                    "add-incoming"
-                )
-            }, // first
-            facadeState.bankAccount // second
-        )
-    }
-    val client = HttpClient { followRedirects = true }
-    val resp = client.post(fromDb.first) {
-        setBody(currentBody)
-        basicAuth("exchange", "x")
-        contentType(ContentType.Application.Json)
-        expectSuccess = false
-    }
-    // Sandbox itself failed.  Responding Bad Gateway because here is a proxy.
-    if (resp.status.value.toString().startsWith('5')) {
-        logger.error("Sandbox failed with status code: 
${resp.status.description}")
-        throw badGateway("Sandbox failed at creating the 'admin/add-incoming' 
payment")
-    }
-    // Echo back whatever error is left, because that should be the client 
fault.
-    if (!resp.status.value.toString().startsWith('2')) {
-        logger.error("Client-side error for /admin/add-incoming.  Sandbox 
says: ${resp.bodyAsText()}")
-        call.respond(resp.status, resp.bodyAsText())
-    }
-    // x-libeufin-bank-ingest
-    val ingestionResult = ingestXLibeufinBankMessage(
-        fromDb.second,
-        resp.bodyAsText()
-    )
-    if (ingestionResult.newTransactions != 1)
-        throw internalServerError("/admin/add-incoming was ingested into 
${ingestionResult.newTransactions} new transactions, but it must have one.")
-    if (ingestionResult.errors != null) {
-        val errors = ingestionResult.errors
-        errors?.forEach {
-            logger.error(it.message)
-        }
-        throw internalServerError("/admin/add-incoming ingestion failed.")
-    }
-    // TWG ingest.
-    ingestFacadeTransactions(
-        bankAccountId = fromDb.second,
-        facadeType = NexusFacadeType.TALER,
-        incomingFilterCb = ::talerFilter,
-        refundCb = ::maybeTalerRefunds
-    )
-    /**
-     * The latest incoming payment should now be found among
-     * the ingested ones.
-     */
-    val lastIncomingPayment = transaction {
-        val allIncomingPayments = TalerIncomingPaymentEntity.all()
-        /**
-         * One payment must appear, since it was created BY this handler.
-         * If not, then respond 500.
-         */
-        if (allIncomingPayments.empty())
-            throw internalServerError("Incoming payment(s) not found AFTER 
/add-incoming")
-        val lastRecord = allIncomingPayments.last()
-        return@transaction Pair(lastRecord.id.value, lastRecord.timestampMs)
-    }
-    call.respond(object {
-        val row_id = lastIncomingPayment.first
-        val timestamp = GnunetTimestamp(lastIncomingPayment.second / 1000L)
-    })
-}
-
-private fun getCurrency(facadeName: String): String {
-    return transaction {
-        getFacadeState(facadeName).currency
-    }
-}
-
-fun talerFacadeRoutes(route: Route) {
-    route.get("/config") {
-        val facadeId = ensureNonNull(call.parameters["fcid"])
-        call.request.requirePermission(
-            PermissionQuery("facade", facadeId, 
"facade.talerwiregateway.transfer"),
-            PermissionQuery("facade", facadeId, 
"facade.talerwiregateway.history")
-        )
-        call.respond(object {
-            val version = "0:0:0"
-            val name = "taler-wire-gateway"
-            val currency = getCurrency(facadeId)
-        })
-        return@get
-    }
-    route.post("/transfer") {
-        talerTransfer(call)
-        return@post
-    }
-    route.get("/history/outgoing") {
-        historyOutgoing(call)
-        return@get
-    }
-    route.get("/history/incoming") {
-        historyIncoming(call)
-        return@get
-    }
-    route.post("/admin/add-incoming") {
-        addIncoming(call)
-        return@post
-    }
-    route.get("") {
-        call.respondText("Hello, this is a Taler Facade")
-        return@get
-    }
-}
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
deleted file mode 100644
index 0be5f876..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ /dev/null
@@ -1,456 +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.nexus.bankaccount
-
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.server.application.ApplicationCall
-import io.ktor.client.HttpClient
-import io.ktor.http.HttpStatusCode
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.iso20022.*
-import tech.libeufin.nexus.server.*
-import tech.libeufin.nexus.xlibeufinbank.ingestXLibeufinBankMessage
-import tech.libeufin.util.XMLUtil
-import tech.libeufin.util.internalServerError
-import java.time.Instant
-import java.time.ZoneOffset
-import java.time.ZonedDateTime
-
-private val keepBankMessages: String? = 
System.getenv("LIBEUFIN_NEXUS_KEEP_BANK_MESSAGES")
-
-fun requireBankAccount(call: ApplicationCall, parameterKey: String): 
NexusBankAccountEntity {
-    val name = call.parameters[parameterKey]
-    if (name == null)
-        throw NexusError(
-            HttpStatusCode.InternalServerError,
-            "no parameter for bank account"
-        )
-    val account = transaction { NexusBankAccountEntity.findByName(name) }
-    if (account == null) {
-        throw NexusError(HttpStatusCode.NotFound, "bank connection '$name' not 
found")
-    }
-    return account
-}
-
-suspend fun submitPaymentInitiation(httpClient: HttpClient, 
paymentInitiationId: Long) {
-    val r = transaction {
-        val paymentInitiation = 
PaymentInitiationEntity.findById(paymentInitiationId)
-        if (paymentInitiation == null) {
-            throw NexusError(HttpStatusCode.NotFound, "prepared payment not 
found")
-        }
-        object {
-            val type = 
paymentInitiation.bankAccount.defaultBankConnection?.type
-            val submitted = paymentInitiation.submitted
-        }
-    }
-    // Skips, if the payment was sent once already.
-    if (r.submitted) {
-        return
-    }
-    if (r.type == null)
-        throw NexusError(HttpStatusCode.NotFound, "no default bank connection")
-
-    getConnectionPlugin(r.type).submitPaymentInitiation(httpClient, 
paymentInitiationId)
-}
-
-/**
- * Submit all pending prepared payments.
- */
-suspend fun submitAllPaymentInitiations(
-    httpClient: HttpClient,
-    accountid: String
-) {
-    data class Submission(val id: Long)
-    val workQueue = mutableListOf<Submission>()
-    transaction {
-        val account = NexusBankAccountEntity.findByName(accountid) ?: throw 
NexusError(
-            HttpStatusCode.NotFound,
-            "account not found"
-        )
-        /**
-         * Skip submitted and invalid preparations.
-         */
-        PaymentInitiationEntity.find {
-            // Not submitted.
-            (PaymentInitiationsTable.submitted eq false) and
-                    // From the correct bank account.
-            (PaymentInitiationsTable.bankAccount eq account.id)
-        }.forEach {
-            if (it.invalid == true) return@forEach
-            val defaultBankConnectionId = 
it.bankAccount.defaultBankConnection?.id ?: throw NexusError(
-                HttpStatusCode.NotFound,
-                "Default bank connection not found.  Can't submit Pain 
document"
-            )
-            // Rare, but filter out bank accounts without a bank connection.
-            val bankConnection = 
NexusBankConnectionEntity.findById(defaultBankConnectionId) ?: throw NexusError(
-                HttpStatusCode.InternalServerError,
-                "Bank connection '$defaultBankConnectionId' " +
-                        "(pointed by bank account 
'${it.bankAccount.bankAccountName}')" +
-                        " not found in the database."
-            )
-            try { 
BankConnectionType.parseBankConnectionType(bankConnection.type) }
-            catch (e: Exception) {
-                logger.info("Skipping non-implemented bank connection 
'${bankConnection.type}'")
-                return@forEach
-            }
-            workQueue.add(Submission(it.id.value))
-        }
-    }
-    workQueue.forEach { submitPaymentInitiation(httpClient, it.id) }
-}
-
-/**
- * NOTE: this type can be used BOTH for one Camt document OR
- * for a set of those.
- */
-data class IngestedTransactionsCount(
-    /**
-     * Number of transactions that are new to the database.
-     * Note that transaction T can be downloaded multiple times;
-     * for example, once in a C52 and once - maybe a day later -
-     * in a C53.  The second time, the transaction is not considered
-     * 'new'.
-     */
-    val newTransactions: Int,
-
-    /**
-     * Total number of transactions that were included in a report
-     * or a statement.
-     */
-    val downloadedTransactions: Int,
-    /**
-     * Exceptions occurred while fetching transactions.  Fetching
-     * transactions can be done via multiple EBICS messages, therefore
-     * a failing one should not prevent other messages to be fetched.
-     * This list collects all the exceptions that happened while fetching
-     * multiple messages.
-     */
-    var errors: List<Exception>? = null
-)
-
-/**
- * Causes new Nexus transactions to be stored into the database.  Note:
- * this function does NOT parse itself the banking data but relies on the
- * dedicated helpers.  This function is mostly responsible for _iterating_
- * over the new downloaded messages and update the local bank account about
- * the new data.
- */
-fun ingestBankMessagesIntoAccount(
-    bankConnectionId: String,
-    bankAccountId: String
-): IngestedTransactionsCount {
-    var totalNew = 0
-    var downloadedTransactions = 0
-    transaction {
-        val conn =
-            NexusBankConnectionEntity.find {
-                NexusBankConnectionsTable.connectionId eq bankConnectionId
-            }.firstOrNull()
-        if (conn == null) {
-            throw NexusError(HttpStatusCode.InternalServerError, "connection 
not found")
-        }
-        val acct = NexusBankAccountEntity.findByName(bankAccountId)
-        if (acct == null) {
-            throw NexusError(HttpStatusCode.InternalServerError, "account not 
found")
-        }
-        var lastId = acct.highestSeenBankMessageSerialId
-        /**
-         * This block picks all the new messages that were downloaded
-         * from the bank and passes them to the deeper banking data handlers
-         * according to the connection type.  Such handlers are then 
responsible
-         * to extract the interesting values and insert them into the database.
-         */
-        NexusBankMessageEntity.find {
-            (NexusBankMessagesTable.bankConnection eq conn.id) and
-                    (NexusBankMessagesTable.id greater 
acct.highestSeenBankMessageSerialId) and
-                    not(NexusBankMessagesTable.errors)
-        }.orderBy(
-            Pair(NexusBankMessagesTable.id, SortOrder.ASC)
-        ).forEach {
-            val ingestionResult: IngestedTransactionsCount = 
when(BankConnectionType.parseBankConnectionType(conn.type)) {
-                BankConnectionType.EBICS -> {
-                    val camtString = it.message.bytes.toString(Charsets.UTF_8)
-                    /**
-                     * NOT validating _again_ the camt document because it was
-                     * already validate before being stored into the database.
-                     */
-                    val doc = XMLUtil.parseStringIntoDom(camtString)
-                    /**
-                     * Calling the CaMt handler.  After its return, all the 
Neuxs-meaningful
-                     * payment data got stored into the database and is ready 
to being further
-                     * processed by any facade OR simply be communicated to 
the CLI via JSON.
-                     */
-                    try {
-                        ingestCamtMessageIntoAccount(
-                            bankAccountId,
-                            doc,
-                            it.fetchLevel,
-                            conn.dialect
-                        )
-                    }
-                    catch (e: Exception) {
-                        logger.error("Could not parse the following camt 
document:\n${camtString}")
-                        // rethrowing. Here just to log the failing document
-                        throw e
-                    }
-                }
-                BankConnectionType.X_LIBEUFIN_BANK -> {
-                    val jMessage = try { 
jacksonObjectMapper().readTree(it.message.bytes) }
-                    catch (e: Exception) {
-                        logger.error("Bank message ${it.id}/${it.messageId} 
could not" +
-                                " be parsed into JSON by the x-libeufin-bank 
ingestion.")
-                        throw internalServerError("Could not ingest 
x-libeufin-bank messages.")
-                    }
-                    ingestXLibeufinBankMessage(
-                        bankAccountId,
-                        jMessage
-                    )
-                }
-            }
-            /**
-             * Checking for errors.  Note: errors do NOT stop this loop as
-             * they mean that ONE message has errors.  Erroneous messages gets
-             * (1) flagged, (2) skipped when this function will run again, and 
(3)
-             * NEVER deleted from the database.
-             */
-            if (ingestionResult.newTransactions == -1) {
-                it.errors = true
-                lastId = it.id.value
-                return@forEach
-            }
-            totalNew += ingestionResult.newTransactions
-            downloadedTransactions += ingestionResult.downloadedTransactions
-            /**
-             * Disk-space conservative check: only store if "yes" was
-             * explicitly set into the environment variable.  Any other
-             * value or non given falls back to deletion.
-             */
-            if (keepBankMessages == null || keepBankMessages != "yes") {
-                it.delete()
-                return@forEach
-            }
-            /**
-             * Updating the highest seen message ID with the serial ID of
-             * the row that's being currently iterated over.  Note: this
-             * number is ever-growing REGARDLESS of the row being kept into
-             * the database.
-             */
-            lastId = it.id.value
-        }
-        // Causing the lastId to be stored into the database:
-        acct.highestSeenBankMessageSerialId = lastId
-    }
-    return IngestedTransactionsCount(
-        newTransactions = totalNew,
-        downloadedTransactions = downloadedTransactions
-    )
-}
-
-data class LastMessagesTimes(
-    val lastStatement: ZonedDateTime?,
-    val lastReport: ZonedDateTime?,
-    val lastNotification: ZonedDateTime?
-)
-/**
- * Get the last timestamps where a report and
- * a statement were received for the bank account
- * given as argument.
- */
-fun getLastMessagesTimes(bankAccountId: String): LastMessagesTimes {
-    val acct = getBankAccount(bankAccountId)
-    return getLastMessagesTimes(acct)
-}
-
-fun getLastMessagesTimes(acct: NexusBankAccountEntity): LastMessagesTimes {
-    return LastMessagesTimes(
-        lastReport = acct.lastReportCreationTimestamp?.let {
-            ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
-        },
-        lastStatement = acct.lastStatementCreationTimestamp?.let {
-            ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
-        },
-        lastNotification = acct.lastNotificationCreationTimestamp?.let {
-            ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
-        }
-    )
-}
-
-fun addPaymentInitiation(paymentData: Pain001Data, debtorAccount: String): 
PaymentInitiationEntity {
-    val bankAccount = getBankAccount(debtorAccount)
-    return addPaymentInitiation(paymentData, bankAccount)
-}
-
-suspend fun fetchBankAccountTransactions(
-    client: HttpClient,
-    fetchSpec: FetchSpecJson,
-    accountId: String
-): IngestedTransactionsCount {
-    val connectionDetails = transaction {
-        val acct = NexusBankAccountEntity.findByName(accountId)
-        if (acct == null) {
-            throw NexusError(
-                HttpStatusCode.NotFound,
-                "Account '$accountId' not found"
-            )
-        }
-        val conn = acct.defaultBankConnection
-        if (conn == null) {
-            throw NexusError(
-                HttpStatusCode.BadRequest,
-                "No default bank connection (explicit connection not yet 
supported)"
-            )
-        }
-        return@transaction object {
-            /**
-             * The connection type _as enum_ should eventually come
-             * directly from the database, instead of being parsed by
-             * parseBankConnectionType().
-             */
-            val connectionType = 
BankConnectionType.parseBankConnectionType(conn.type)
-            val connectionName = conn.connectionId
-        }
-    }
-    /**
-     * Collects transactions from the bank and stores the (camt)
-     * document into the database.  This function tries to download
-     * both reports AND statements even if the first one fails.
-     */
-    val errors: List<Exception>? = 
getConnectionPlugin(connectionDetails.connectionType).fetchTransactions(
-        fetchSpec,
-        client,
-        connectionDetails.connectionName,
-        accountId
-    )
-    /**
-     * Here it MIGHT just return in case of errors, but sometimes the
-     * fetcher asks for multiple results (e.g. C52 and C53), and what
-     * went through SHOULD be ingested.
-     */
-
-    /**
-     * This block causes new NexusBankAccountTransactions rows to be
-     * INSERTed into the database, according to the banking data that
-     * was recently downloaded.
-     */
-    val ingestionResult: IngestedTransactionsCount = 
ingestBankMessagesIntoAccount(
-        connectionDetails.connectionName,
-        accountId
-    )
-    /**
-     * The following two functions further process the banking data
-     * that was recently downloaded, according to the particular facade
-     * being honored.
-     */
-    ingestFacadeTransactions(
-        bankAccountId = accountId,
-        facadeType = NexusFacadeType.TALER,
-        incomingFilterCb = ::talerFilter,
-        refundCb = ::maybeTalerRefunds
-    )
-    ingestFacadeTransactions(
-        bankAccountId = accountId,
-        facadeType = NexusFacadeType.ANASTASIS,
-        incomingFilterCb = ::anastasisFilter,
-        refundCb = null
-    )
-
-    ingestionResult.errors = errors
-    return ingestionResult
-}
-
-fun importBankAccount(call: ApplicationCall, offeredBankAccountId: String, 
nexusBankAccountId: String) {
-    transaction {
-        val conn = requireBankConnection(call, "connid")
-        // first get handle of the offered bank account
-        val offeredAccount = OfferedBankAccountsTable.select {
-            OfferedBankAccountsTable.offeredAccountId eq offeredBankAccountId 
and
-                    (OfferedBankAccountsTable.bankConnection eq conn.id.value)
-        }.firstOrNull() ?: throw NexusError(
-            HttpStatusCode.NotFound, "Could not find offered bank account 
'${offeredBankAccountId}'"
-        )
-        // detect name collisions first.
-        NexusBankAccountEntity.findByName(nexusBankAccountId).run {
-            // This variable will either host a new, or a found imported bank 
account.
-            val importedAccount = when (this) {
-                is NexusBankAccountEntity -> {
-                    if (this.iban != 
offeredAccount[OfferedBankAccountsTable.iban]) {
-                        throw NexusError(
-                            HttpStatusCode.Conflict,
-                            "$nexusBankAccountId exists already and its IBAN 
is different from $offeredBankAccountId"
-                        )
-                    }
-                    // an imported bank account already exists and
-                    // the user tried to import the same IBAN to it.  Do 
nothing
-                    this
-                }
-                // such named imported account didn't exist.  Make it
-                else -> {
-                    val newImportedAccount = NexusBankAccountEntity.new {
-                        bankAccountName = nexusBankAccountId
-                        iban = offeredAccount[OfferedBankAccountsTable.iban]
-                        bankCode = 
offeredAccount[OfferedBankAccountsTable.bankCode]
-                        defaultBankConnection = conn
-                        highestSeenBankMessageSerialId = 0
-                        accountHolder = 
offeredAccount[OfferedBankAccountsTable.accountHolder]
-                    }
-                    logger.info("Account ${newImportedAccount.id} gets 
imported")
-                    newImportedAccount
-                }
-            }
-            // Associate the bank account as named by the bank (the 'offered')
-            // with the imported/local one (the 'imported').  Rewrites are 
acceptable.
-            OfferedBankAccountsTable.update(
-                {
-                    OfferedBankAccountsTable.offeredAccountId eq 
offeredBankAccountId and
-                            (OfferedBankAccountsTable.bankConnection eq 
conn.id.value)
-                }
-            ) {
-                it[imported] = importedAccount.id
-            }
-        }
-    }
-}
-
-
-/**
- * Check if the transaction is already found in the database.
- * This function works as long as the caller provides the appropriate
- * 'uid' parameter.  For CaMt messages this value is carried along
- * the AcctSvcrRef node, whereas for x-libeufin-bank connections
- * that's the 'uid' field of the XLibeufinBankTransaction type.
- *
- * Returns the transaction that's already in the database, in case
- * the 'uid' is from a duplicate.
- */
-fun findDuplicate(
-    bankAccountId: String,
-    uid: String
-): NexusBankTransactionEntity? {
-    return transaction {
-        val account = NexusBankAccountEntity.findByName((bankAccountId)) ?:
-        return@transaction null
-        NexusBankTransactionEntity.find {
-            (NexusBankTransactionsTable.accountTransactionId eq uid) and
-                    (NexusBankTransactionsTable.bankAccount eq account.id)
-        }.firstOrNull()
-    }
-}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
deleted file mode 100644
index d77a8b38..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
+++ /dev/null
@@ -1,437 +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/>
- */
-
-/**
- * High-level interface for the EBICS protocol.
- */
-package tech.libeufin.nexus.ebics
-
-import io.ktor.client.HttpClient
-import io.ktor.client.plugins.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import tech.libeufin.nexus.NexusError
-import tech.libeufin.util.*
-import java.util.*
-
-private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util")
-
-private suspend inline fun HttpClient.postToBank(url: String, body: String): 
String {
-    if (!XMLUtil.validateFromString(body)) throw NexusError(
-        HttpStatusCode.InternalServerError,
-        "EBICS (outgoing) document is invalid"
-    )
-    val response: HttpResponse = try {
-        this.post(urlString = url) {
-                setBody(body)
-        }
-    } catch (e: ClientRequestException) {
-        logger.error("Exception during request to $url: ${e.message}")
-        val returnStatus = if (e.response.status.value == 
HttpStatusCode.RequestTimeout.value)
-            HttpStatusCode.GatewayTimeout
-        else HttpStatusCode.BadGateway
-        throw NexusError(
-            returnStatus,
-            e.message
-        )
-    }
-    catch (e: Exception) {
-        logger.error("Exception during request to $url: ${e.message}")
-        throw NexusError(
-            HttpStatusCode.BadGateway,
-            e.message ?: "Could not reach the bank"
-        )
-    }
-    /**
-     * EBICS should be expected only after a 200 OK response
-     * (including problematic ones); throw exception in all the other cases,
-     * by echoing what the bank said.
-     */
-    if (response.status.value != HttpStatusCode.OK.value)
-        throw NexusError(
-            HttpStatusCode.BadGateway,
-            "bank says: ${response.bodyAsText()}"
-        )
-    return response.bodyAsText()
-}
-
-sealed class EbicsDownloadResult
-
-class EbicsDownloadSuccessResult(
-    val orderData: ByteArray,
-    /**
-     * This value points at the EBICS transaction that carried
-     * the order data contained in this structure.  That makes
-     * possible to log the EBICS transaction that carried one
-     * invalid order data, for example.
-     */
-    val transactionID: String? = null
-) : EbicsDownloadResult()
-
-class EbicsDownloadEmptyResult(
-    val orderData: ByteArray = ByteArray(0)
-) : EbicsDownloadResult()
-
-
-/**
- * A bank-technical error occurred.
- */
-class EbicsDownloadBankErrorResult(
-    val returnCode: EbicsReturnCode
-) : EbicsDownloadResult()
-
-/**
- * Do an EBICS download transaction.  This includes
- * the initialization phase, transaction phase and receipt phase.
- */
-suspend fun doEbicsDownloadTransaction(
-    client: HttpClient,
-    subscriberDetails: EbicsClientSubscriberDetails,
-    fetchSpec: EbicsFetchSpec
-): EbicsDownloadResult {
-
-    // Initialization phase
-    val initDownloadRequestStr = if (fetchSpec.isEbics3) {
-        if (fetchSpec.ebics3Service == null)
-            throw internalServerError("Expected EBICS 3 fetch spec but null 
was found.")
-        createEbicsRequestForDownloadInitialization(
-            subscriberDetails,
-            fetchSpec.ebics3Service,
-            fetchSpec.orderParams
-        )
-    }
-    else {
-        if (fetchSpec.orderType == null)
-            throw internalServerError("Expected EBICS 2.5 order type but null 
was found.")
-        createEbicsRequestForDownloadInitialization(
-            subscriberDetails,
-            fetchSpec.orderType,
-            fetchSpec.orderParams
-        )
-    }
-    val payloadChunks = LinkedList<String>()
-    val initResponseStr = client.postToBank(subscriberDetails.ebicsUrl, 
initDownloadRequestStr)
-    val initResponse = parseAndValidateEbicsResponse(
-        subscriberDetails,
-        initResponseStr,
-        withEbics3 = fetchSpec.isEbics3
-    )
-    val transactionID: String? = initResponse.transactionID
-    // Checking for EBICS communication problems.
-    when (initResponse.technicalReturnCode) {
-        EbicsReturnCode.EBICS_OK -> {
-            /**
-             * The EBICS communication succeeded, but business problems
-             * may be reported along the 'bank technical' code; this check
-             * takes place later.
-             */
-        }
-        else -> {
-            // The bank gave a valid XML response but EBICS had problems.
-            throw EbicsProtocolError(
-                HttpStatusCode.UnprocessableEntity,
-                "EBICS-technical error at init phase: " +
-                        "${initResponse.technicalReturnCode} 
${initResponse.reportText}," +
-                        " for fetching level ${fetchSpec.originalLevel} and 
transaction ID: $transactionID.",
-                initResponse.technicalReturnCode
-            )
-        }
-    }
-    // Checking the 'bank technical' code.
-    when (initResponse.bankReturnCode) {
-        EbicsReturnCode.EBICS_OK -> {
-            // Success, nothing to do!
-        }
-        EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE -> {
-            // The 'pf' dialect might respond this value here (at init phase),
-            // in contrast to what the default dialect does (waiting the 
transfer phase)
-            return EbicsDownloadEmptyResult()
-        }
-        else -> {
-            println("Bank raw response: $initResponseStr")
-            logger.error(
-                "Bank-technical error at init phase: 
${initResponse.bankReturnCode}" +
-                        ", for fetching level ${fetchSpec.originalLevel} and 
transaction ID $transactionID."
-            )
-            return EbicsDownloadBankErrorResult(initResponse.bankReturnCode)
-        }
-    }
-    logger.debug("Bank acknowledges EBICS download initialization." +
-            "  Transaction ID: $transactionID.")
-    val encryptionInfo = initResponse.dataEncryptionInfo
-        ?: throw NexusError(
-            HttpStatusCode.BadGateway,
-            "Initial response did not contain encryption info.  " +
-                    "Fetching level ${fetchSpec.originalLevel} , transaction 
ID $transactionID"
-        )
-
-    val initOrderDataEncChunk = initResponse.orderDataEncChunk
-        ?: throw NexusError(
-            HttpStatusCode.BadGateway,
-            "Initial response for download transaction does not " +
-                    "contain data transfer.  Fetching level 
${fetchSpec.originalLevel}, " +
-                    "transaction ID $transactionID."
-        )
-    payloadChunks.add(initOrderDataEncChunk)
-
-    val numSegments = initResponse.numSegments
-        ?: throw NexusError(
-            HttpStatusCode.FailedDependency,
-            "Missing segment number in EBICS download init response." +
-                    "  Fetching level ${fetchSpec.originalLevel}, transaction 
ID $transactionID"
-        )
-    // Transfer phase
-    for (x in 2 .. numSegments) {
-        val transferReqStr =
-            createEbicsRequestForDownloadTransferPhase(
-                subscriberDetails,
-                transactionID,
-                x,
-                numSegments,
-                fetchSpec.isEbics3
-            )
-        logger.debug("EBICS download transfer phase of ${transactionID}: 
sending segment $x")
-        val transferResponseStr = 
client.postToBank(subscriberDetails.ebicsUrl, transferReqStr)
-        val transferResponse = 
parseAndValidateEbicsResponse(subscriberDetails, transferResponseStr)
-        when (transferResponse.technicalReturnCode) {
-            EbicsReturnCode.EBICS_OK -> {
-                // Success, nothing to do!
-            }
-            else -> {
-                throw NexusError(
-                    HttpStatusCode.FailedDependency,
-                    "EBICS-technical error at transfer phase: " +
-                            "${transferResponse.technicalReturnCode} 
${transferResponse.reportText}." +
-                            "  Fetching level ${fetchSpec.originalLevel}, 
transaction ID $transactionID"
-                )
-            }
-        }
-        when (transferResponse.bankReturnCode) {
-            EbicsReturnCode.EBICS_OK -> {
-                // Success, nothing to do!
-            }
-            else -> {
-                logger.error("Bank-technical error at transfer phase: " +
-                        "${transferResponse.bankReturnCode}." +
-                        "  Fetching level ${fetchSpec.originalLevel}, 
transaction ID $transactionID")
-                return 
EbicsDownloadBankErrorResult(transferResponse.bankReturnCode)
-            }
-        }
-        val transferOrderDataEncChunk = transferResponse.orderDataEncChunk
-            ?: throw NexusError(
-                HttpStatusCode.BadGateway,
-                "transfer response for download transaction " +
-                        "does not contain data transfer.  Fetching level 
${fetchSpec.originalLevel}, transaction ID $transactionID"
-            )
-        payloadChunks.add(transferOrderDataEncChunk)
-        logger.debug("Download transfer phase of ${transactionID}: bank 
acknowledges $x")
-    }
-
-    val respPayload = decryptAndDecompressResponse(subscriberDetails, 
encryptionInfo, payloadChunks)
-
-    // Acknowledgement phase
-    val ackRequest = createEbicsRequestForDownloadReceipt(
-        subscriberDetails,
-        transactionID,
-        fetchSpec.isEbics3
-    )
-    val ackResponseStr = client.postToBank(
-        subscriberDetails.ebicsUrl,
-        ackRequest
-    )
-    val ackResponse = parseAndValidateEbicsResponse(
-        subscriberDetails,
-        ackResponseStr,
-        withEbics3 = fetchSpec.isEbics3
-    )
-    when (ackResponse.technicalReturnCode) {
-        EbicsReturnCode.EBICS_DOWNLOAD_POSTPROCESS_DONE -> {
-        }
-        else -> {
-            throw NexusError(
-                HttpStatusCode.InternalServerError,
-                "Unexpected EBICS return code" +
-                        " at acknowledgement phase: 
${ackResponse.technicalReturnCode.name}." +
-                        "  Fetching level ${fetchSpec.originalLevel}, 
transaction ID $transactionID"
-            )
-        }
-    }
-    logger.debug("Bank acknowledges EBICS download receipt.  Transaction ID: 
$transactionID.")
-    return EbicsDownloadSuccessResult(respPayload, transactionID)
-}
-
-// Currently only 1-segment requests.
-suspend fun doEbicsUploadTransaction(
-    client: HttpClient,
-    subscriberDetails: EbicsClientSubscriberDetails,
-    uploadSpec: EbicsUploadSpec,
-    payload: ByteArray
-) {
-    if (subscriberDetails.bankEncPub == null) {
-        throw NexusError(HttpStatusCode.BadRequest,
-            "bank encryption key unknown, request HPB first"
-        )
-    }
-    val preparedUploadData = prepareUploadPayload(
-        subscriberDetails,
-        payload,
-        isEbics3 = uploadSpec.isEbics3
-    )
-    val req: String = if (uploadSpec.isEbics3) {
-        if (uploadSpec.ebics3Service == null)
-            throw internalServerError("EBICS 3 service data was expected, but 
null was found.")
-        createEbicsRequestForUploadInitialization(
-            subscriberDetails,
-            uploadSpec.ebics3Service,
-            uploadSpec.orderParams,
-            preparedUploadData
-        )
-    } else {
-        if (uploadSpec.orderType == null)
-            throw internalServerError("EBICS 2.5 order type was expected, but 
null was found.")
-        createEbicsRequestForUploadInitialization(
-            subscriberDetails,
-            uploadSpec.orderType,
-            uploadSpec.orderParams ?: EbicsStandardOrderParams(),
-            preparedUploadData
-        )
-    }
-    logger.debug("EBICS upload message to: ${subscriberDetails.ebicsUrl}")
-    val responseStr = client.postToBank(subscriberDetails.ebicsUrl, req)
-
-    val initResponse = parseAndValidateEbicsResponse(
-        subscriberDetails,
-        responseStr,
-        withEbics3 = uploadSpec.isEbics3
-    )
-    // The bank indicated one error, hence Nexus sent invalid data.
-    if (initResponse.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
-        throw NexusError(
-            HttpStatusCode.InternalServerError,
-            reason = "EBICS-technical error at init phase:" +
-                    " ${initResponse.technicalReturnCode} 
${initResponse.reportText}"
-        )
-    }
-    // The bank did NOT indicate any error, but the response
-    // lacks required information, blame the bank.
-    val transactionID = initResponse.transactionID
-    if (initResponse.bankReturnCode != EbicsReturnCode.EBICS_OK) {
-        throw NexusError(
-            HttpStatusCode.InternalServerError,
-            reason = "Bank-technical error at init phase:" +
-                    " ${initResponse.bankReturnCode}"
-        )
-    }
-    logger.debug("Bank acknowledges EBICS upload initialization. " +
-            " Transaction ID: $transactionID.")
-
-    /* now send actual payload */
-    val ebicsPayload = createEbicsRequestForUploadTransferPhase(
-        subscriberDetails,
-        transactionID,
-        preparedUploadData,
-        0,
-        withEbics3 = uploadSpec.isEbics3
-    )
-    val txRespStr = client.postToBank(
-        subscriberDetails.ebicsUrl,
-        ebicsPayload
-    )
-    val txResp = parseAndValidateEbicsResponse(
-        subscriberDetails,
-        txRespStr,
-        withEbics3 = uploadSpec.isEbics3
-    )
-    when (txResp.technicalReturnCode) {
-        EbicsReturnCode.EBICS_OK -> {/* do nothing */}
-        else -> {
-            // EBICS failed, blame Nexus.
-            throw EbicsProtocolError(
-                httpStatusCode = HttpStatusCode.InternalServerError,
-                reason = txResp.reportText,
-                ebicsTechnicalCode = txResp.technicalReturnCode
-            )
-        }
-    }
-    when (txResp.bankReturnCode) {
-        EbicsReturnCode.EBICS_OK -> {/* do nothing */}
-        else -> {
-            /**
-             * Although EBICS went fine, the bank complained about
-             * the communication content.
-             */
-            throw EbicsProtocolError(
-                httpStatusCode = HttpStatusCode.UnprocessableEntity,
-                reason = "bank-technical error: ${txResp.reportText}"
-            )
-        }
-    }
-    logger.debug("Bank acknowledges EBICS upload transfer.  Transaction ID: 
$transactionID")
-}
-
-suspend fun doEbicsHostVersionQuery(client: HttpClient, ebicsBaseUrl: String, 
ebicsHostId: String): EbicsHevDetails {
-    val ebicsHevRequest = makeEbicsHEVRequestRaw(ebicsHostId)
-    val resp = client.postToBank(ebicsBaseUrl, ebicsHevRequest)
-    return parseEbicsHEVResponse(resp)
-}
-
-suspend fun doEbicsIniRequest(
-    client: HttpClient,
-    subscriberDetails: EbicsClientSubscriberDetails
-): EbicsKeyManagementResponseContent {
-    val request = makeEbicsIniRequest(subscriberDetails)
-    val respStr = client.postToBank(
-        subscriberDetails.ebicsUrl,
-        request
-    )
-    return parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, 
respStr)
-}
-
-suspend fun doEbicsHiaRequest(
-    client: HttpClient,
-    subscriberDetails: EbicsClientSubscriberDetails
-): EbicsKeyManagementResponseContent {
-    val request = makeEbicsHiaRequest(subscriberDetails)
-    val respStr = client.postToBank(
-        subscriberDetails.ebicsUrl,
-        request
-    )
-    return parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, 
respStr)
-}
-
-
-suspend fun doEbicsHpbRequest(
-    client: HttpClient,
-    subscriberDetails: EbicsClientSubscriberDetails
-): HpbResponseData {
-    val request = makeEbicsHpbRequest(subscriberDetails)
-    val respStr = client.postToBank(
-        subscriberDetails.ebicsUrl,
-        request
-    )
-    val parsedResponse = 
parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, respStr)
-    val orderData = parsedResponse.orderData ?: throw EbicsProtocolError(
-        HttpStatusCode.BadGateway,
-        "Cannot find data in a HPB response"
-    )
-    return parseEbicsHpbOrder(orderData)
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
deleted file mode 100644
index 1548efce..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
+++ /dev/null
@@ -1,1247 +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/>
- */
-
-/**
- * Handlers for EBICS-related endpoints offered by the nexus for EBICS
- * connections.
- */
-package tech.libeufin.nexus.ebics
-
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import com.itextpdf.kernel.pdf.PdfDocument
-import com.itextpdf.kernel.pdf.PdfWriter
-import com.itextpdf.layout.Document
-import com.itextpdf.layout.element.AreaBreak
-import com.itextpdf.layout.element.Paragraph
-import io.ktor.server.application.call
-import io.ktor.client.HttpClient
-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.server.routing.Route
-import io.ktor.server.routing.post
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.insert
-import org.jetbrains.exposed.sql.select
-import org.jetbrains.exposed.sql.statements.api.ExposedBlob
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.bankaccount.getLastMessagesTimes
-import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData
-import tech.libeufin.nexus.iso20022.createPain001document
-import tech.libeufin.nexus.logger
-import tech.libeufin.nexus.server.*
-import tech.libeufin.util.*
-import tech.libeufin.util.ebics_h004.EbicsTypes
-import tech.libeufin.util.ebics_h004.HTDResponseOrderData
-import tech.libeufin.util.ebics_h005.Ebics3Request
-import java.io.ByteArrayOutputStream
-import java.security.interfaces.RSAPrivateCrtKey
-import java.security.interfaces.RSAPublicKey
-import java.time.*
-import java.time.format.DateTimeFormatter
-import java.util.*
-import javax.crypto.EncryptedPrivateKeyInfo
-
-
-/**
- * This type maps the abstract fetch specifications -- as for example
- * they were given via the Nexus JSON API -- to the specific EBICS type.
- */
-data class EbicsFetchSpec(
-    val orderType: String? = null, // unused for 3.0
-    val orderParams: EbicsOrderParams,
-    val ebics3Service: Ebics3Request.OrderDetails.Service? = null, // unused 
for 2.5
-    // Not always available, for example at raw POST 
/download/${ebicsMessageName} calls.
-    // It helps to trace back the original level.
-    val originalLevel: FetchLevel? = null,
-    val isEbics3: Boolean = false
-)
-
-/**
- * Collects EBICS 2.5 and/or 3.0 parameters for a unified
- * way of passing parameters.  Individual helpers will then
- * act according to the EBICS version.
- */
-data class EbicsUploadSpec(
-    val isEbics3: Boolean = false,
-    val ebics3Service: Ebics3Request.OrderDetails.Service? = null, // unused 
for 2.5
-    val orderType: String? = null,
-    val orderParams: EbicsOrderParams? = null
-)
-
-// Validate and store the received document for later ingestion.
-private fun validateAndStoreCamt(
-    bankConnectionId: String,
-    camt: String,
-    fetchLevel: FetchLevel,
-    transactionID: String? = null, // the EBICS transaction that carried this 
camt.
-    validateBankContent: Boolean = false
-) {
-    val camtDoc = try {
-        XMLUtil.parseStringIntoDom(camt)
-    }
-    catch (e: Exception) {
-        throw badGateway("Could not parse camt document from EBICS transaction 
$transactionID")
-    }
-    if (validateBankContent && !XMLUtil.validateFromDom(camtDoc)) {
-        logger.error("This document didn't validate: $camt")
-        throw badGateway("Camt document from EBICS transaction $transactionID 
is invalid")
-    }
-
-    val msgId = 
camtDoc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId")
-    logger.info("Camt document '$msgId' received via $fetchLevel.")
-    transaction {
-        val conn = NexusBankConnectionEntity.findByName(bankConnectionId)
-        if (conn == null) {
-            throw NexusError(
-                HttpStatusCode.InternalServerError,
-                "bank connection missing"
-            )
-        }
-        val oldMsg = NexusBankMessageEntity.find { 
NexusBankMessagesTable.messageId eq msgId }.firstOrNull()
-        if (oldMsg == null) {
-            NexusBankMessageEntity.new {
-                this.bankConnection = conn
-                this.fetchLevel = fetchLevel
-                this.messageId = msgId
-                this.message = ExposedBlob(camt.toByteArray(Charsets.UTF_8))
-            }
-        }
-    }
-}
-
-private fun handleEbicsDownloadResult(
-    bankResponse: EbicsDownloadResult,
-    bankConnectionId: String,
-    fetchLevel: FetchLevel
-) {
-    when (bankResponse) {
-        is EbicsDownloadSuccessResult -> {
-            bankResponse.orderData.unzipWithLambda {
-                // logger.debug("Camt entry (filename (in the Zip archive): 
${it.first}): ${it.second}")
-                validateAndStoreCamt(
-                    bankConnectionId,
-                    it.second,
-                    fetchLevel,
-                    transactionID = bankResponse.transactionID
-                )
-            }
-        }
-        is EbicsDownloadBankErrorResult -> {
-            throw NexusError(
-                HttpStatusCode.BadGateway,
-                bankResponse.returnCode.errorCode
-            )
-        }
-        is EbicsDownloadEmptyResult -> {
-            // no-op
-        }
-    }
-}
-
-// Fetch EBICS transactions according to the specifications
-// (fetchSpec) it finds in the parameters.
-private suspend fun fetchEbicsTransactions(
-    fetchSpec: EbicsFetchSpec,
-    client: HttpClient,
-    bankConnectionId: String,
-    subscriberDetails: EbicsClientSubscriberDetails,
-) {
-    /**
-     * In this case Nexus will not be able to associate the future
-     * EBICS response with the fetch level originally requested by
-     * the caller, and therefore refuses to continue the execution.
-     * This condition is however in some cases allowed: for example
-     * along the "POST /download/$ebicsMessageType" call, where the result
-     * is not supposed to be stored in the database and therefore doesn't
-     * need its original level.
-     */
-    if (fetchSpec.originalLevel == null) {
-        throw internalServerError(
-            "Original fetch level missing, won't download from EBICS"
-        )
-    }
-    val response: EbicsDownloadResult = try {
-        doEbicsDownloadTransaction(
-            client,
-            subscriberDetails,
-            fetchSpec
-        )
-    } catch (e: EbicsProtocolError) {
-        /**
-         * Although given a error type, an empty transactions list does
-         * not mean anything wrong.
-         */
-        if (e.ebicsTechnicalCode == 
EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) {
-            logger.debug("EBICS had no new data")
-            return
-        }
-        // re-throw in any other error case.
-        throw e
-    }
-    handleEbicsDownloadResult(
-        response,
-        bankConnectionId,
-        fetchSpec.originalLevel
-    )
-}
-
-/**
- * Prepares key material and other EBICS details and
- * returns them along a convenient object.
- */
-private fun getEbicsSubscriberDetailsInternal(subscriber: 
EbicsSubscriberEntity): EbicsClientSubscriberDetails {
-    var bankAuthPubValue: RSAPublicKey? = null
-    if (subscriber.bankAuthenticationPublicKey != null) {
-        bankAuthPubValue = CryptoUtil.loadRsaPublicKey(
-            subscriber.bankAuthenticationPublicKey?.bytes!!
-        )
-    }
-    var bankEncPubValue: RSAPublicKey? = null
-    if (subscriber.bankEncryptionPublicKey != null) {
-        bankEncPubValue = CryptoUtil.loadRsaPublicKey(
-            subscriber.bankEncryptionPublicKey?.bytes!!
-        )
-    }
-    return EbicsClientSubscriberDetails(
-        bankAuthPub = bankAuthPubValue,
-        bankEncPub = bankEncPubValue,
-
-        ebicsUrl = subscriber.ebicsURL,
-        hostId = subscriber.hostID,
-        userId = subscriber.userID,
-        partnerId = subscriber.partnerID,
-
-        customerSignPriv = 
CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.bytes),
-        customerAuthPriv = 
CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.bytes),
-        customerEncPriv = 
CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.bytes),
-        ebicsIniState = subscriber.ebicsIniState,
-        ebicsHiaState = subscriber.ebicsHiaState
-    )
-}
-private fun getSubscriberFromConnection(connectionEntity: 
NexusBankConnectionEntity): EbicsSubscriberEntity =
-    transaction {
-        EbicsSubscriberEntity.find {
-            NexusEbicsSubscribersTable.nexusBankConnection eq 
connectionEntity.id
-        }.firstOrNull() ?: throw internalServerError("ebics bank connection 
'${connectionEntity.connectionId}' has no subscriber.")
-    }
-/**
- * Retrieve Ebics subscriber details given a bank connection.
- */
-fun getEbicsSubscriberDetails(bankConnectionId: String): 
EbicsClientSubscriberDetails {
-    val transport = getBankConnection(bankConnectionId)
-    val subscriber = getSubscriberFromConnection(transport)
-
-    // transport exists and belongs to caller.
-    val ret = getEbicsSubscriberDetailsInternal(subscriber)
-    if (transport.dialect != null)
-        ret.dialect = transport.dialect
-    return ret
-}
-
-fun Route.ebicsBankProtocolRoutes(client: HttpClient) {
-    post("test-host") {
-        val r = call.receive<EbicsHostTestRequest>()
-        val qr = doEbicsHostVersionQuery(client, r.ebicsBaseUrl, r.ebicsHostId)
-        call.respond(qr)
-        return@post
-    }
-}
-
-fun Route.ebicsBankConnectionRoutes(client: HttpClient) {
-    post("/send-ini") {
-        requireSuperuser(call.request)
-        val subscriber = transaction {
-            val conn = requireBankConnection(call, "connid")
-            if (conn.type != "ebics") {
-                throw NexusError(
-                    HttpStatusCode.BadRequest,
-                    "bank connection is not of type 'ebics' (but 
'${conn.type}')"
-                )
-            }
-            getEbicsSubscriberDetails(conn.connectionId)
-        }
-        val resp = doEbicsIniRequest(client, subscriber)
-        call.respond(resp)
-    }
-
-    post("/send-hia") {
-        requireSuperuser(call.request)
-        val subscriber = transaction {
-            val conn = requireBankConnection(call, "connid")
-            if (conn.type != "ebics") {
-                throw NexusError(HttpStatusCode.BadRequest, "bank connection 
is not of type 'ebics'")
-            }
-            getEbicsSubscriberDetails(conn.connectionId)
-        }
-        val resp = doEbicsHiaRequest(client, subscriber)
-        call.respond(resp)
-    }
-
-    post("/send-hev") {
-        requireSuperuser(call.request)
-        val subscriber = transaction {
-            val conn = requireBankConnection(call, "connid")
-            if (conn.type != "ebics") {
-                throw NexusError(HttpStatusCode.BadRequest, "bank connection 
is not of type 'ebics'")
-            }
-            getEbicsSubscriberDetails(conn.connectionId)
-        }
-        val resp = doEbicsHostVersionQuery(client, subscriber.ebicsUrl, 
subscriber.hostId)
-        call.respond(resp)
-    }
-
-    post("/send-hpb") {
-        requireSuperuser(call.request)
-        val subscriberDetails = transaction {
-            val conn = requireBankConnection(call, "connid")
-            if (conn.type != "ebics") {
-                throw NexusError(HttpStatusCode.BadRequest, "bank connection 
is not of type 'ebics'")
-            }
-            getEbicsSubscriberDetails(conn.connectionId)
-        }
-        val hpbData = doEbicsHpbRequest(client, subscriberDetails)
-        transaction {
-            val conn = requireBankConnection(call, "connid")
-            val subscriber =
-                EbicsSubscriberEntity.find { 
NexusEbicsSubscribersTable.nexusBankConnection eq conn.id }.first()
-            subscriber.bankAuthenticationPublicKey = 
ExposedBlob((hpbData.authenticationPubKey.encoded))
-            subscriber.bankEncryptionPublicKey = 
ExposedBlob((hpbData.encryptionPubKey.encoded))
-        }
-        call.respond(object {})
-    }
-
-    // Directly import accounts.  Used for testing.
-    post("/import-accounts") {
-        requireSuperuser(call.request)
-        val subscriberDetails = transaction {
-            val conn = requireBankConnection(call, "connid")
-            if (conn.type != "ebics") {
-                throw NexusError(HttpStatusCode.BadRequest, "bank connection 
is not of type 'ebics'")
-            }
-            getEbicsSubscriberDetails(conn.connectionId)
-        }
-        val response = doEbicsDownloadTransaction(
-            client,
-            subscriberDetails,
-            EbicsFetchSpec(
-                orderType = "HTD",
-                orderParams = EbicsStandardOrderParams()
-            )
-        )
-        when (response) {
-            is EbicsDownloadEmptyResult -> {
-                // no-op
-                logger.warn("HTD response was empty.")
-            }
-            is EbicsDownloadBankErrorResult -> {
-                throw NexusError(
-                    HttpStatusCode.BadGateway,
-                    response.returnCode.errorCode
-                )
-            }
-            is EbicsDownloadSuccessResult -> {
-                val payload = 
XMLUtil.convertStringToJaxb<HTDResponseOrderData>(
-                    response.orderData.toString(Charsets.UTF_8)
-                )
-                transaction {
-                    val conn = requireBankConnection(call, "connid")
-                    payload.value.partnerInfo.accountInfoList?.forEach {
-                        NexusBankAccountEntity.new {
-                            bankAccountName = it.id
-                            accountHolder = it.accountHolder ?: "NOT-GIVEN"
-                            iban = 
it.accountNumberList?.filterIsInstance<EbicsTypes.GeneralAccountNumber>()
-                                ?.find { it.international }?.value
-                                ?: throw NexusError(HttpStatusCode.NotFound, 
reason = "bank gave no IBAN")
-                            bankCode = 
it.bankCodeList?.filterIsInstance<EbicsTypes.GeneralBankCode>()
-                                ?.find { it.international }?.value
-                                ?: throw NexusError(
-                                    HttpStatusCode.NotFound,
-                                    reason = "bank gave no BIC"
-                                )
-                            defaultBankConnection = conn
-                            highestSeenBankMessageSerialId = 0
-                        }
-                    }
-                }
-                response.orderData.toString(Charsets.UTF_8)
-            }
-        }
-        call.respond(object {})
-    }
-
-    post("/download/{msgtype}") {
-        requireSuperuser(call.request)
-        val orderType = 
requireNotNull(call.parameters["msgtype"]).uppercase(Locale.ROOT)
-        if (orderType.length != 3) {
-            throw NexusError(HttpStatusCode.BadRequest, "ebics order type must 
be three characters")
-        }
-        val paramsJson = 
call.receiveNullable<EbicsStandardOrderParamsEmptyJson>()
-        val orderParams = paramsJson?.toOrderParams() ?: 
EbicsStandardOrderParams()
-        val subscriberDetails = transaction {
-            val conn = requireBankConnection(call, "connid")
-            if (conn.type != "ebics") {
-                throw NexusError(HttpStatusCode.BadRequest, "bank connection 
is not of type 'ebics'")
-            }
-            getEbicsSubscriberDetails(conn.connectionId)
-        }
-        val response = doEbicsDownloadTransaction(
-            client,
-            subscriberDetails,
-            EbicsFetchSpec(
-                orderType = orderType,
-                orderParams = orderParams,
-                ebics3Service = null,
-                originalLevel = null
-            )
-        )
-        when (response) {
-            is EbicsDownloadEmptyResult -> {
-                logger.info(orderType + " response was empty.") // no op
-            }
-            is EbicsDownloadSuccessResult -> {
-                call.respondText(
-                    response.orderData.toString(Charsets.UTF_8),
-                    ContentType.Text.Plain,
-                    HttpStatusCode.OK
-                )
-            }
-            is EbicsDownloadBankErrorResult -> {
-                call.respond(
-                    HttpStatusCode.BadGateway,
-                    NexusErrorJson(
-                        error = NexusErrorDetailJson(
-                            type = "bank-error",
-                            description = response.returnCode.errorCode
-                        )
-                    )
-                )
-            }
-        }
-    }
-}
-
-/**
- * Do the Hpb request when we don't know whether our keys have been submitted 
or not.
- *
- * Return true when the tentative HPB request succeeded, and thus key 
initialization is done.
- */
-private suspend fun tentativeHpb(client: HttpClient, connId: String): Boolean {
-    val subscriber = transaction { getEbicsSubscriberDetails(connId) }
-    val hpbData = try {
-        doEbicsHpbRequest(client, subscriber)
-    } catch (e: EbicsProtocolError) {
-        logger.info("failed tentative hpb request", e)
-        return false
-    }
-    transaction {
-        val conn = NexusBankConnectionEntity.findByName(connId)
-        if (conn == null) {
-            throw NexusError(HttpStatusCode.NotFound, "bank connection 
'$connId' not found")
-        }
-        val subscriberEntity =
-            EbicsSubscriberEntity.find { 
NexusEbicsSubscribersTable.nexusBankConnection eq conn.id }.first()
-        subscriberEntity.ebicsIniState = EbicsInitState.SENT
-        subscriberEntity.ebicsHiaState = EbicsInitState.SENT
-        subscriberEntity.bankAuthenticationPublicKey =
-            ExposedBlob((hpbData.authenticationPubKey.encoded))
-        subscriberEntity.bankEncryptionPublicKey = 
ExposedBlob((hpbData.encryptionPubKey.encoded))
-    }
-    return true
-}
-
-fun formatHex(ba: ByteArray): String {
-    var out = ""
-    for (i in ba.indices) {
-        val b = ba[i]
-        if (i > 0 && i % 16 == 0) {
-            out += "\n"
-        }
-        out += java.lang.String.format("%02X", b)
-        out += " "
-    }
-    return out
-}
-
-// A null return value indicates that the connection uses EBICS 3.0
-private fun getSubmissionTypeAfterDialect(dialect: String? = null): String? {
-    return when (dialect) {
-        "pf" -> null // "XE2"
-        else -> "CCT"
-    }
-}
-
-private fun getStatementSpecAfterDialect(dialect: String? = null, p: 
EbicsOrderParams): EbicsFetchSpec {
-    return when (dialect) {
-        "pf" -> EbicsFetchSpec(
-            orderType = "Z53",
-            orderParams = p,
-            ebics3Service = Ebics3Request.OrderDetails.Service().apply {
-                serviceName = "EOP"
-                messageName = 
Ebics3Request.OrderDetails.Service.MessageName().apply {
-                    value = "camt.053"
-                    version = "04"
-                }
-                scope = "CH"
-                container = 
Ebics3Request.OrderDetails.Service.Container().apply {
-                    containerType = "ZIP"
-                }
-            },
-            originalLevel = FetchLevel.STATEMENT
-        )
-        else -> EbicsFetchSpec(
-            orderType = "C53",
-            orderParams = p,
-            ebics3Service = null,
-            originalLevel = FetchLevel.STATEMENT
-        )
-    }
-}
-
-private fun getNotificationSpecAfterDialect(dialect: String? = null, p: 
EbicsOrderParams): EbicsFetchSpec {
-    return when (dialect) {
-        "pf" -> EbicsFetchSpec(
-            orderType = null, // triggers 3.0
-            orderParams = p,
-            ebics3Service = Ebics3Request.OrderDetails.Service().apply {
-                serviceName = "REP"
-                messageName = 
Ebics3Request.OrderDetails.Service.MessageName().apply {
-                    value = "camt.054"
-                    version = "08"
-                }
-                scope = "CH"
-                container = 
Ebics3Request.OrderDetails.Service.Container().apply {
-                    containerType = "ZIP"
-                }
-            },
-            originalLevel = FetchLevel.NOTIFICATION,
-            isEbics3 = true
-        )
-        else -> EbicsFetchSpec(
-            orderType = "C54",
-            orderParams = p,
-            ebics3Service = null,
-            originalLevel = FetchLevel.NOTIFICATION
-        )
-    }
-}
-private fun getReportSpecAfterDialect(dialect: String? = null, p: 
EbicsOrderParams): EbicsFetchSpec {
-    return when (dialect) {
-        "pf" -> EbicsFetchSpec(
-            orderType = "Z52",
-            orderParams = p,
-            ebics3Service = null,
-            originalLevel = FetchLevel.REPORT
-        )
-        else -> EbicsFetchSpec(
-            orderType = "C52",
-            orderParams = p,
-            ebics3Service = null,
-            originalLevel = FetchLevel.REPORT
-        )
-    }
-}
-
-/**
- * This function returns a possibly empty list of Exception.
- * That helps not to stop fetching if ONE operation fails.  Notably,
- * C52 and C53 may be asked along one invocation of this function,
- * therefore storing the exception on C52 allows the C53 to still
- * take place.  The caller then decides how to handle the exceptions.
- */
-class EbicsBankConnectionProtocol: BankConnectionProtocol {
-    /**
-     * Downloads the pain.002 that informs about previous
-     * payments submissions.  Not all the banks offer this
-     * service; some may use analog channels.
-     */
-    suspend fun fetchPaymentReceipt(
-        fetchSpec: FetchSpecJson,
-        client: HttpClient,
-        bankConnectionId: String,
-        accountId: String
-    ) {
-        val subscriberDetails = transaction { 
getEbicsSubscriberDetails(bankConnectionId) }
-        // Typically a date range.
-        if (fetchSpec.level != FetchLevel.RECEIPT) {
-            logger.error("This method accepts only RECEIPT as the fetch level, 
not '${fetchSpec.level}'.")
-            throw badRequest("Invalid params to get payments receipts: use 
fetch level RECEIPT.")
-        }
-        val ebicsOrderInfo = when(fetchSpec) {
-            is FetchSpecLatestJson -> {
-                EbicsFetchSpec(
-                    orderType = "Z01", // PoFi specific.
-                    orderParams = EbicsStandardOrderParams(),
-                    originalLevel = fetchSpec.level
-                )
-            }
-            else -> throw NotImplementedError("Fetch spec 
'${fetchSpec::class}' not supported for payment receipts.")
-        }
-        // Proceeding to download now.
-        val response = try {
-            doEbicsDownloadTransaction(
-                client,
-                subscriberDetails,
-                EbicsFetchSpec(
-                    orderType = ebicsOrderInfo.orderType,
-                    orderParams = ebicsOrderInfo.orderParams
-                )
-            )
-        } catch (e: EbicsProtocolError) {
-            if (e.ebicsTechnicalCode == 
EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) {
-                logger.debug("EBICS had no new data")
-                return
-            }
-            // re-throw in any other error case.
-            throw e
-        }
-        when(response) {
-            is EbicsDownloadEmptyResult -> {
-                // no-op
-            }
-            is EbicsDownloadBankErrorResult -> {
-                logger.error("Bank technical code: ${response.returnCode}")
-            }
-            is EbicsDownloadSuccessResult -> {
-                // Extracting the content (pain.002) and parsing it.
-                val orderData: String = response.orderData.toString()
-                logger.debug(orderData)
-                val doc = XMLUtil.parseStringIntoDom(orderData)
-
-            }
-        }
-    }
-
-    override fun getBankUrl(connId: String): String {
-        val subscriberDetails = transaction { 
getEbicsSubscriberDetails(connId) }
-        return subscriberDetails.ebicsUrl
-    }
-    override suspend fun fetchTransactions(
-        fetchSpec: FetchSpecJson,
-        client: HttpClient,
-        bankConnectionId: String,
-        accountId: String
-    ): List<Exception>? {
-        val subscriberDetails = transaction { 
getEbicsSubscriberDetails(bankConnectionId) }
-        val lastTimes = getLastMessagesTimes(accountId)
-        /**
-         * Will be filled with fetch instructions, according
-         * to the parameters received from the client.
-         */
-        val specs = mutableListOf<EbicsFetchSpec>()
-        /**
-         * 'level' indicates whether to fetch statements and/or reports,
-         * whereas 'p' usually carries a date range.
-         */
-        fun addForLevel(l: FetchLevel, p: EbicsOrderParams) {
-            when (l) {
-                FetchLevel.ALL -> {
-                    
specs.add(getReportSpecAfterDialect(subscriberDetails.dialect, p))
-                    
specs.add(getStatementSpecAfterDialect(subscriberDetails.dialect, p))
-                }
-                FetchLevel.REPORT -> {
-                    
specs.add(getReportSpecAfterDialect(subscriberDetails.dialect, p))
-                }
-                FetchLevel.STATEMENT -> {
-                    
specs.add(getStatementSpecAfterDialect(subscriberDetails.dialect, p))
-                }
-                FetchLevel.NOTIFICATION -> {
-                    
specs.add(getNotificationSpecAfterDialect(subscriberDetails.dialect, p))
-                }
-                else -> {
-                    logger.error("fetch level wrong in addForLevel() helper: 
${fetchSpec.level}.")
-                    throw badRequest("Fetch level ${fetchSpec.level} not 
supported")
-                }
-            }
-        }
-        // Figuring out what time range to put in the fetch instructions.
-        when (fetchSpec) {
-            is FetchSpecTimeRangeJson -> {
-                // the parse() method defaults to the YYYY-MM-DD format.
-                // If parsing fails, the global catcher intervenes.
-
-                val start: LocalDate = parseDashedDate(fetchSpec.start)
-                val end: LocalDate = parseDashedDate(fetchSpec.end)
-                val p = EbicsStandardOrderParams(
-                    EbicsDateRange(
-                        start = 
start.atStartOfDay().atZone(ZoneId.systemDefault()),
-                        end = end.atStartOfDay().atZone(ZoneId.systemDefault())
-                    )
-                )
-                addForLevel(fetchSpec.level, p)
-            }
-            is FetchSpecLatestJson -> {
-                val p = EbicsStandardOrderParams()
-                addForLevel(fetchSpec.level, p)
-            }
-            /**
-             * This spec wants _all_ the records, therefore the
-             * largest time frame possible needs to be specified.
-             * Rarely employed in production, but useful for tests.
-             */
-            is FetchSpecAllJson -> {
-                val start = ZonedDateTime.ofInstant(
-                    Instant.EPOCH,
-                    ZoneOffset.UTC
-                )
-                val end = ZonedDateTime.ofInstant(
-                    /**
-                     * XML date sets the time to 'start of the day'.  By
-                     * adding 24 hours, we make sure today's transactions
-                     * are included in the response.
-                     */
-                    Instant.now().plusSeconds(60 * 60 * 24),
-                    ZoneOffset.systemDefault()
-                )
-                val p = EbicsStandardOrderParams(
-                    EbicsDateRange(start, end)
-                )
-                addForLevel(fetchSpec.level, p)
-            }
-            /**
-             * This branch differentiates the last date of reports and
-             * statements and builds the fetch instructions for each of
-             * them.  For this reason, it does not use the "addForLevel()"
-             * helper, since that uses the same date for all the messages
-             * falling in the ALL level.
-             */
-            is FetchSpecSinceLastJson -> {
-                val pRep = EbicsStandardOrderParams(
-                    EbicsDateRange(
-                        lastTimes.lastReport ?: ZonedDateTime.ofInstant(
-                            Instant.EPOCH,
-                            ZoneOffset.UTC
-                        ), ZonedDateTime.now(ZoneOffset.UTC)
-                    )
-                )
-                val pStmt = EbicsStandardOrderParams(
-                    EbicsDateRange(
-                        lastTimes.lastStatement ?: ZonedDateTime.ofInstant(
-                            Instant.EPOCH,
-                            ZoneOffset.UTC
-                        ), ZonedDateTime.now(ZoneOffset.UTC)
-                    )
-                )
-                val pNtfn = EbicsStandardOrderParams(
-                    EbicsDateRange(
-                        lastTimes.lastNotification ?: ZonedDateTime.ofInstant(
-                            Instant.EPOCH,
-                            ZoneOffset.UTC
-                        ), ZonedDateTime.now(ZoneOffset.UTC)
-                    )
-                )
-                when (fetchSpec.level) {
-                    FetchLevel.ALL -> {
-                        
specs.add(getReportSpecAfterDialect(subscriberDetails.dialect, pRep))
-                        
specs.add(getStatementSpecAfterDialect(subscriberDetails.dialect, pRep))
-                    }
-                    FetchLevel.REPORT -> {
-                        
specs.add(getReportSpecAfterDialect(subscriberDetails.dialect, pRep))
-                    }
-                    FetchLevel.STATEMENT -> {
-                        
specs.add(getStatementSpecAfterDialect(subscriberDetails.dialect, pStmt))
-                    }
-                    FetchLevel.NOTIFICATION -> {
-                        
specs.add(getNotificationSpecAfterDialect(subscriberDetails.dialect, pNtfn))
-                    }
-                    else -> throw badRequest("Fetch level ${fetchSpec.level} " 
+
-                            "not supported in the 'since last' EBICS time 
range.")
-                }
-            }
-        }
-        // Downloads and stores the bank message into the database.  No 
ingestion.
-        val errors = mutableListOf<Exception>()
-        for (spec in specs) {
-            try {
-                fetchEbicsTransactions(
-                    spec,
-                    client,
-                    bankConnectionId,
-                    subscriberDetails
-                )
-            } catch (e: Exception) {
-                logger.warn("Fetching transactions (${spec.originalLevel}) 
excepted: ${e.message}.")
-                e.printStackTrace()
-                errors.add(e)
-            }
-        }
-        if (errors.size > 0)
-            return errors
-        return null
-    }
-
-    // Submit one Pain.001 for one payment initiations.
-    override suspend fun submitPaymentInitiation(
-        httpClient: HttpClient,
-        paymentInitiationId: Long
-    ) {
-        val dbData = transaction {
-            val preparedPayment = getPaymentInitiation(paymentInitiationId)
-            val conn = preparedPayment.bankAccount.defaultBankConnection ?: 
throw NexusError(
-                HttpStatusCode.NotFound,
-                "no default bank connection available for submission"
-            )
-            val subscriberDetails = 
getEbicsSubscriberDetails(conn.connectionId)
-            val painMessage = createPain001document(
-                NexusPaymentInitiationData(
-                    debtorIban = preparedPayment.bankAccount.iban,
-                    debtorBic = preparedPayment.bankAccount.bankCode,
-                    debtorName = preparedPayment.bankAccount.accountHolder,
-                    currency = preparedPayment.currency,
-                    amount = preparedPayment.sum,
-                    creditorIban = preparedPayment.creditorIban,
-                    creditorName = preparedPayment.creditorName,
-                    creditorBic = preparedPayment.creditorBic,
-                    paymentInformationId = 
preparedPayment.paymentInformationId,
-                    preparationTimestamp = preparedPayment.preparationDate,
-                    subject = preparedPayment.subject,
-                    instructionId = preparedPayment.instructionId,
-                    endToEndId = preparedPayment.endToEndId,
-                    messageId = preparedPayment.messageId
-                ),
-                dialect = subscriberDetails.dialect
-            )
-            object {
-                val painXml = painMessage
-                val subscriberDetails = subscriberDetails
-            }
-        }
-        val isPoFi = dbData.subscriberDetails.dialect == "pf"
-        val uploadSpec = EbicsUploadSpec(
-            isEbics3 = isPoFi,
-            orderType = if (!isPoFi) 
getSubmissionTypeAfterDialect(dbData.subscriberDetails.dialect) else null,
-            orderParams = EbicsStandardOrderParams(),
-            ebics3Service = if (isPoFi)
-                Ebics3Request.OrderDetails.Service().apply {
-                    serviceName = "MCT"
-                    scope = "CH"
-                    messageName = 
Ebics3Request.OrderDetails.Service.MessageName().apply {
-                        value = "pain.001"
-                        version = "09"
-                    }
-                }
-                else null
-        )
-        doEbicsUploadTransaction(
-            httpClient,
-            dbData.subscriberDetails,
-            uploadSpec,
-            dbData.painXml.toByteArray(Charsets.UTF_8)
-        )
-        transaction {
-            val payment = getPaymentInitiation(paymentInitiationId)
-            payment.submitted = true
-            payment.submissionDate = LocalDateTime.now().millis()
-        }
-    }
-
-    override fun exportAnalogDetails(conn: NexusBankConnectionEntity): 
ByteArray {
-        val ebicsSubscriber = transaction { 
getEbicsSubscriberDetails(conn.connectionId) }
-        val po = ByteArrayOutputStream()
-        val pdfWriter = PdfWriter(po)
-        val pdfDoc = PdfDocument(pdfWriter)
-        val date = LocalDateTime.now()
-        val dateStr = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
-
-        fun writeCommon(doc: Document) {
-            doc.add(
-                Paragraph(
-                    """
-            Datum: $dateStr
-            Teilnehmer: ${conn.id.value}
-            Host-ID: ${ebicsSubscriber.hostId}
-            User-ID: ${ebicsSubscriber.userId}
-            Partner-ID: ${ebicsSubscriber.partnerId}
-            ES version: A006
-        """.trimIndent()
-                )
-            )
-        }
-
-        fun writeKey(doc: Document, priv: RSAPrivateCrtKey) {
-            val pub = CryptoUtil.getRsaPublicFromPrivate(priv)
-            val hash = CryptoUtil.getEbicsPublicKeyHash(pub)
-            
doc.add(Paragraph("Exponent:\n${formatHex(pub.publicExponent.toByteArray())}"))
-            
doc.add(Paragraph("Modulus:\n${formatHex(pub.modulus.toByteArray())}"))
-            doc.add(Paragraph("SHA-256 hash:\n${formatHex(hash)}"))
-        }
-
-        fun writeSigLine(doc: Document) {
-            doc.add(Paragraph("Ort / Datum: ________________"))
-            doc.add(Paragraph("Firma / Name: ________________"))
-            doc.add(Paragraph("Unterschrift: ________________"))
-        }
-
-        Document(pdfDoc).use {
-            it.add(Paragraph("Signaturschlüssel").setFontSize(24f))
-            writeCommon(it)
-            it.add(Paragraph("Öffentlicher Schlüssel (Public key for the 
electronic signature)"))
-            writeKey(it, ebicsSubscriber.customerSignPriv)
-            it.add(Paragraph("\n"))
-            writeSigLine(it)
-            it.add(AreaBreak())
-
-            it.add(Paragraph("Authentifikationsschlüssel").setFontSize(24f))
-            writeCommon(it)
-            it.add(Paragraph("Öffentlicher Schlüssel (Public key for the 
identification and authentication signature)"))
-            writeKey(it, ebicsSubscriber.customerAuthPriv)
-            it.add(Paragraph("\n"))
-            writeSigLine(it)
-            it.add(AreaBreak())
-
-            it.add(Paragraph("Verschlüsselungsschlüssel").setFontSize(24f))
-            writeCommon(it)
-            it.add(Paragraph("Öffentlicher Schlüssel (Public encryption key)"))
-            writeKey(it, ebicsSubscriber.customerEncPriv)
-            it.add(Paragraph("\n"))
-            writeSigLine(it)
-        }
-        pdfWriter.flush()
-        return po.toByteArray()
-    }
-
-    override fun exportBackup(bankConnectionId: String, passphrase: String): 
JsonNode {
-        val subscriber = transaction { 
getEbicsSubscriberDetails(bankConnectionId) }
-        val ret = EbicsKeysBackupJson(
-            type = "ebics",
-            dialect = subscriber.dialect,
-            userID = subscriber.userId,
-            hostID = subscriber.hostId,
-            partnerID = subscriber.partnerId,
-            ebicsURL = subscriber.ebicsUrl,
-            authBlob = bytesToBase64(
-                CryptoUtil.encryptKey(
-                    subscriber.customerAuthPriv.encoded,
-                    passphrase
-                )
-            ),
-            encBlob = bytesToBase64(
-                CryptoUtil.encryptKey(
-                    subscriber.customerEncPriv.encoded,
-                    passphrase
-                )
-            ),
-            sigBlob = bytesToBase64(
-                CryptoUtil.encryptKey(
-                    subscriber.customerSignPriv.encoded,
-                    passphrase
-                )
-            ),
-            bankAuthBlob = run {
-                val maybeBankAuthPub = subscriber.bankAuthPub
-                if (maybeBankAuthPub != null)
-                    return@run bytesToBase64(maybeBankAuthPub.encoded)
-                null
-            },
-            bankEncBlob = run {
-                val maybeBankEncPub = subscriber.bankEncPub
-                if (maybeBankEncPub != null)
-                    return@run bytesToBase64(maybeBankEncPub.encoded)
-                null
-            }
-        )
-        val mapper = ObjectMapper()
-        return mapper.valueToTree(ret)
-    }
-
-    override fun getConnectionDetails(conn: NexusBankConnectionEntity): 
JsonNode {
-        val ebicsSubscriber = transaction { 
getEbicsSubscriberDetails(conn.connectionId) }
-        val mapper = ObjectMapper()
-        val details = mapper.createObjectNode()
-        details.put("ebicsUrl", ebicsSubscriber.ebicsUrl)
-        details.put("ebicsHostId", ebicsSubscriber.hostId)
-        details.put("partnerId", ebicsSubscriber.partnerId)
-        details.put("userId", ebicsSubscriber.userId)
-        details.put(
-            "customerAuthKeyHash",
-            CryptoUtil.getEbicsPublicKeyHash(
-                
CryptoUtil.getRsaPublicFromPrivate(ebicsSubscriber.customerAuthPriv)
-            ).toHexString()
-        )
-        details.put(
-            "customerEncKeyHash",
-            CryptoUtil.getEbicsPublicKeyHash(
-                
CryptoUtil.getRsaPublicFromPrivate(ebicsSubscriber.customerEncPriv)
-            ).toHexString()
-        )
-        val bankAuthPubImmutable = ebicsSubscriber.bankAuthPub
-        if (bankAuthPubImmutable != null) {
-            details.put(
-                "bankAuthKeyHash",
-                
CryptoUtil.getEbicsPublicKeyHash(bankAuthPubImmutable).toHexString()
-            )
-        }
-        val bankEncPubImmutable = ebicsSubscriber.bankEncPub
-        if (bankEncPubImmutable != null) {
-            details.put(
-                "bankEncKeyHash",
-                
CryptoUtil.getEbicsPublicKeyHash(bankEncPubImmutable).toHexString()
-            )
-        }
-        val node = mapper.createObjectNode()
-        node.put("type", conn.type)
-        node.put("owner", conn.owner.username)
-        node.put("ready", true) // test with #6715 needed.
-        node.set<JsonNode>("details", details)
-        return node
-    }
-    override fun createConnection(
-        connId: String,
-        user: NexusUserEntity,
-        data: JsonNode
-    ) {
-        val newTransportData = jacksonObjectMapper()
-            .treeToValue(data, EbicsNewTransport::class.java) ?: throw 
NexusError(
-            HttpStatusCode.BadRequest,
-            "Ebics details not found in request"
-        )
-        val bankConn = NexusBankConnectionEntity.new {
-            this.connectionId = connId
-            owner = user
-            type = "ebics"
-            this.dialect = newTransportData.dialect
-        }
-        val pairA = CryptoUtil.generateRsaKeyPair(2048)
-        val pairB = CryptoUtil.generateRsaKeyPair(2048)
-        val pairC = CryptoUtil.generateRsaKeyPair(2048)
-        EbicsSubscriberEntity.new {
-            ebicsURL = newTransportData.ebicsURL
-            hostID = newTransportData.hostID
-            partnerID = newTransportData.partnerID
-            userID = newTransportData.userID
-            systemID = newTransportData.systemID
-            signaturePrivateKey = ExposedBlob((pairA.private.encoded))
-            encryptionPrivateKey = ExposedBlob((pairB.private.encoded))
-            authenticationPrivateKey = ExposedBlob((pairC.private.encoded))
-            nexusBankConnection = bankConn
-            ebicsIniState = EbicsInitState.NOT_SENT
-            ebicsHiaState = EbicsInitState.NOT_SENT
-        }
-    }
-
-    override fun createConnectionFromBackup(
-        connId: String,
-        user: NexusUserEntity,
-        passphrase: String?,
-        backup: JsonNode
-    ) {
-        if (passphrase === null) {
-            throw NexusError(
-                HttpStatusCode.BadRequest,
-                "EBICS backup needs passphrase"
-            )
-        }
-        val ebicsBackup = jacksonObjectMapper().treeToValue(backup, 
EbicsKeysBackupJson::class.java)
-        val bankConn = NexusBankConnectionEntity.new {
-            connectionId = connId
-            owner = user
-            type = "ebics"
-            this.dialect = ebicsBackup.dialect
-        }
-        val (authKey, encKey, sigKey) = try {
-            Triple(
-                CryptoUtil.decryptKey(
-                    
EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.authBlob)),
-                    passphrase
-                ),
-                CryptoUtil.decryptKey(
-                    
EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.encBlob)),
-                    passphrase
-                ),
-                CryptoUtil.decryptKey(
-                    
EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.sigBlob)),
-                    passphrase
-                )
-            )
-        } catch (e: Exception) {
-            e.printStackTrace()
-            logger.info("Restoring keys failed, probably due to wrong 
passphrase")
-            throw NexusError(
-                HttpStatusCode.BadRequest,
-                "Bad backup given"
-            )
-        }
-        try {
-            EbicsSubscriberEntity.new {
-                ebicsURL = ebicsBackup.ebicsURL
-                hostID = ebicsBackup.hostID
-                partnerID = ebicsBackup.partnerID
-                userID = ebicsBackup.userID
-                signaturePrivateKey = ExposedBlob(sigKey.encoded)
-                encryptionPrivateKey = ExposedBlob((encKey.encoded))
-                authenticationPrivateKey = ExposedBlob((authKey.encoded))
-                nexusBankConnection = bankConn
-                ebicsIniState = EbicsInitState.UNKNOWN
-                ebicsHiaState = EbicsInitState.UNKNOWN
-                if (ebicsBackup.bankAuthBlob != null) {
-                    val keyBlob = base64ToBytes(ebicsBackup.bankAuthBlob)
-                    try { CryptoUtil.loadRsaPublicKey(keyBlob) }
-                    catch (e: Exception) {
-                        logger.error("Could not restore bank's auth public 
key")
-                        throw NexusError(
-                            HttpStatusCode.BadRequest,
-                            "Bad bank's auth pub"
-                        )
-                    }
-                    bankAuthenticationPublicKey = ExposedBlob(keyBlob)
-                }
-                if (ebicsBackup.bankEncBlob != null) {
-                    val keyBlob = base64ToBytes(ebicsBackup.bankEncBlob)
-                    try { CryptoUtil.loadRsaPublicKey(keyBlob) }
-                    catch (e: Exception) {
-                        logger.error("Could not restore bank's enc public key")
-                        throw NexusError(
-                            HttpStatusCode.BadRequest,
-                            "Bad bank's enc pub"
-                        )
-                    }
-                    bankEncryptionPublicKey = ExposedBlob(keyBlob)
-                }
-             }
-        } catch (e: Exception) {
-            throw NexusError(
-                HttpStatusCode.BadRequest,
-                "exception: $e"
-            )
-        }
-        return
-    }
-
-    override suspend fun fetchAccounts(client: HttpClient, connId: String) {
-        val subscriberDetails = transaction { 
getEbicsSubscriberDetails(connId) }
-        val response = doEbicsDownloadTransaction(
-            client,
-            subscriberDetails,
-            EbicsFetchSpec(
-                orderType = "HTD",
-                orderParams = EbicsStandardOrderParams()
-            )
-        )
-        when (response) {
-            is EbicsDownloadEmptyResult -> {
-                // no-op
-                logger.warn("HTD response was empty.")
-            }
-            is EbicsDownloadBankErrorResult -> {
-                throw NexusError(
-                    HttpStatusCode.BadGateway,
-                    response.returnCode.errorCode
-                )
-            }
-            is EbicsDownloadSuccessResult -> {
-                val payload = 
XMLUtil.convertStringToJaxb<HTDResponseOrderData>(
-                    response.orderData.toString(Charsets.UTF_8)
-                )
-                transaction {
-                    payload.value.partnerInfo.accountInfoList?.forEach { 
accountInfo ->
-                        val conn = 
NexusBankConnectionEntity.findByName(connId) ?: throw NexusError(
-                            HttpStatusCode.NotFound,
-                            "bank connection not found"
-                        )
-                        // Avoiding to store twice one downloaded bank account.
-                        val isDuplicate = OfferedBankAccountsTable.select {
-                            OfferedBankAccountsTable.bankConnection eq conn.id 
and (
-                                    OfferedBankAccountsTable.offeredAccountId 
eq accountInfo.id)
-                        }.firstOrNull()
-                        if (isDuplicate != null) return@forEach
-                        // Storing every new bank account.
-                        OfferedBankAccountsTable.insert { newRow ->
-                            newRow[accountHolder] = accountInfo.accountHolder 
?: "NOT GIVEN"
-                            newRow[iban] =
-                                
accountInfo.accountNumberList?.filterIsInstance<EbicsTypes.GeneralAccountNumber>()
-                                    ?.find { it.international }?.value
-                                    ?: throw 
NexusError(HttpStatusCode.NotFound, reason = "bank gave no IBAN")
-                            newRow[bankCode] = 
accountInfo.bankCodeList?.filterIsInstance<EbicsTypes.GeneralBankCode>()
-                                ?.find { it.international }?.value
-                                ?: throw NexusError(
-                                    HttpStatusCode.NotFound,
-                                    reason = "bank gave no BIC"
-                                )
-                            newRow[bankConnection] = 
requireBankConnectionInternal(connId).id
-                            newRow[offeredAccountId] = accountInfo.id
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    override suspend fun connect(client: HttpClient, connId: String) {
-        val subscriber = transaction { getEbicsSubscriberDetails(connId) }
-        if (subscriber.bankAuthPub != null && subscriber.bankEncPub != null) {
-            return
-        }
-        if (subscriber.ebicsIniState == EbicsInitState.UNKNOWN || 
subscriber.ebicsHiaState == EbicsInitState.UNKNOWN) {
-            if (tentativeHpb(client, connId)) {
-                /**
-                 * NOTE/FIXME: in case the HIA/INI did succeed (state is 
UNKNOWN but Sandbox
-                 * has somehow the keys), here the state should be set to 
SENT, because later -
-                 * when the Sandbox will respond to the INI/HIA requests - 
we'll get a
-                 * EBICS_INVALID_USER_OR_USER_STATE.  Hence, the state will 
never switch to
-                 * SENT again.
-                 */
-                return
-            }
-        }
-        val iniDone = when (subscriber.ebicsIniState) {
-            EbicsInitState.NOT_SENT, EbicsInitState.UNKNOWN -> {
-                val iniResp = doEbicsIniRequest(client, subscriber)
-                iniResp.bankReturnCode == EbicsReturnCode.EBICS_OK && 
iniResp.technicalReturnCode == EbicsReturnCode.EBICS_OK
-            }
-            EbicsInitState.SENT -> true
-        }
-        val hiaDone = when (subscriber.ebicsHiaState) {
-            EbicsInitState.NOT_SENT, EbicsInitState.UNKNOWN -> {
-                val hiaResp = doEbicsHiaRequest(client, subscriber)
-                hiaResp.bankReturnCode == EbicsReturnCode.EBICS_OK && 
hiaResp.technicalReturnCode == EbicsReturnCode.EBICS_OK
-            }
-            EbicsInitState.SENT -> true
-        }
-        val hpbData = try {
-            doEbicsHpbRequest(client, subscriber)
-        } catch (e: EbicsProtocolError) {
-            logger.warn("failed HPB request", e)
-            null
-        }
-        transaction {
-            val conn = NexusBankConnectionEntity.findByName(connId)
-            if (conn == null) {
-                throw NexusError(HttpStatusCode.NotFound, "bank connection 
'$connId' not found")
-            }
-            val subscriberEntity =
-                EbicsSubscriberEntity.find { 
NexusEbicsSubscribersTable.nexusBankConnection eq conn.id }.first()
-            if (iniDone) {
-                subscriberEntity.ebicsIniState = EbicsInitState.SENT
-            }
-            if (hiaDone) {
-                subscriberEntity.ebicsHiaState = EbicsInitState.SENT
-            }
-            if (hpbData != null) {
-                subscriberEntity.bankAuthenticationPublicKey =
-                    ExposedBlob((hpbData.authenticationPubKey.encoded))
-                subscriberEntity.bankEncryptionPublicKey = 
ExposedBlob((hpbData.encryptionPubKey.encoded))
-            }
-        }
-    }
-}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt
deleted file mode 100644
index 2a83e847..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt
+++ /dev/null
@@ -1,286 +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.nexus.iso20022
-import CreditDebitIndicator
-
-/**
- * Extra rules for German Banking Industry Committee (GBIC) for ISO 20022.
- */
-object GbicRules {
-    /**
-     * Map credit/debit indicator and the German GVC code to a ISO 20022 bank 
transaction code.
-     * When multiple alternatives are available, we always choose the least 
specific one.
-     *
-     * Mapping taken from "Anhang1 zu Anlage 3 - Datenformatstandards-Version 
3.3 Final Version-2019-04-11"
-     */
-    @Suppress("SpellCheckingInspection")
-    fun getBtcFromGvc(c: CreditDebitIndicator, s: String): String {
-        val cd = when (c) {
-            CreditDebitIndicator.CRDT -> "C"
-            CreditDebitIndicator.DBIT -> "D"
-        }
-        return when ("${cd}-${s}") {
-            "D-006" -> "PMNT-CCRD-POSC"
-            "C-058" -> "PMNT-RCDT-FICT"
-            "C-072" -> "PMNT-DRFT-STLR"
-            "D-073" -> "PMNT-DRFT-STAM"
-            "C-079" -> "PMNT-MCOP-OTHR"
-            "D-079" -> "PMNT-MDOP-OTHR"
-            "C-082" -> "PMNT-CNTR-CDPT"
-            "D-083" -> "PMNT-CNTR-CWDL"
-            "D-084" -> "PMNT-RDDT-OODD"
-            "D-087" -> "PMNT-ICDT-SDVA"
-            "C-088" -> "PMNT-RCDT-SDVA"
-            "C-093" -> "PMNT-DRFT-DDFT"
-            "C-095" -> "TRAD-GUAR-OTHR"
-            "D-095" -> "TRAD-GUAR-OTHR"
-            "C-098" -> "PMNT-MCRD-SMCD"
-            "D-101" -> "PMNT-ICHQ-CCHQ"
-            "D-102" -> "PMNT-ICHQ-ORCQ"
-            "D-103" -> "PMNT-ICHQ-CCHQ"
-            "D-104" -> "PMNT-RDDT-BBDD"
-            "D-105" -> "PMNT-RDDT-ESDD"
-            // Alternatives:
-            // "D-106" -> "PMNT-CCRD-CWDL"
-            // "D-106" -> "PMNT-CCRD-SMRT"
-            // "D-106" -> "PMNT-CCRD-POSD"
-            // "D-106" -> "PMNT-MCRD-CHRG"
-            "D-106" -> "PMNT-CCRD-OTHR"
-            "D-107" -> "PMNT-CCRD-OTHR"
-            "D-108" -> "PMNT-IDDT-UPDD"
-            "D-109" -> "PMNT-IDDT-UPDD"
-            "D-110" -> "PMNT-MCRD-UPCT"
-            "D-111" -> "PMNT-ICHQ-UPCQ"
-            "D-112" -> "PMNT-ICHQ-OTHR"
-            "C-112" -> "PMNT-RCHQ-OTHR"
-            "D-116" -> "PMNT-ICDT-ESCT"
-            "D-118" -> "PMNT-IRCT-ESCT"
-            "D-117" -> "PMNT-ICDT-STDO"
-            "D-119" -> "PMNT-ICDT-ESCT"
-            "D-122" -> "PMNT-ICHQ-CCHQ"
-            "C-152" -> "PMNT-RCDT-STDO"
-            "C-153" -> "PMNT-RCDT-SALA"
-            "C-154" -> "PMNT-RCDT-ESCT"
-            "C-155" -> "PMNT-RCDT-ESCT"
-            "C-156" -> "PMNT-RCDT-ESCT"
-            "C-157" -> "PMNT-RRCT-SALA"
-            "C-159" -> "PMNT-ICDT-RRTN"
-            "D-159" -> "PMNT-RCDT-RRTN"
-            "C-160" -> "PMNT-IRCT-RRTN"
-            "D-160" -> "PMNT-RRCT-RRTN"
-            "C-161" -> "PMNT-RRCT-ESCT"
-            "C-162" -> "PMNT-RRCT-ESCT"
-            "C-163" -> "PMNT-RRCT-ESCT"
-            "C-164" -> "PMNT-RRCT-ESCT"
-            "C-165" -> "PMNT-RRCT-ESCT"
-            "C-166" -> "PMNT-RCDT-ESCT"
-            "C-167" -> "PMNT-RCDT-ESCT"
-            "C-168" -> "PMNT-RRCT-ESCT"
-            "C-169" -> "PMNT-RCDT-ESCT"
-            "C-170" -> "PMNT-RCHQ-URCQ"
-            "C-171" -> "PMNT-IDDT-ESDD"
-            "C-174" -> "PMNT-IDDT-BBDD"
-            "D-177" -> "PMNT-ICDT-ESCT"
-            "C-181" -> "PMNT-RDDT-UPDD"
-            "C-182" -> "PMNT-CCRD-RIMB"
-            "C-183" -> "PMNT-RCHQ-UPCQ"
-            "C-184" -> "PMNT-RDDT-UPDD"
-            "D-185" -> "PMNT-ICHQ-CCHQ"
-            "D-188" -> "PMNT-IRCT-ESCT"
-            "C-189" -> "PMNT-RRCT-ESCT"
-            "D-190" -> "PMNT-CCRD-OTHR"
-            "D-191" -> "PMNT-ICDT-ESCT"
-            "C-192" -> "PMNT-IDDT-ESDD"
-            "D-193" -> "PMNT-IDDT-RCDD"
-            "C-194" -> "PMNT-RCDT-ESCT"
-            "D-195" -> "PMNT-RDDT-ESDD"
-            "C-196" -> "PMNT-IDDT-BBDD"
-            "D-197" -> "PMNT-RDDT-BBDD"
-            "C-198" -> "PMNT-MCRD-POSP"
-            "D-199" -> "PMNT-MCRD-DAJT"
-            "D-201" -> "PMNT-ICDT-XBCT"
-            "C-202" -> "PMNT-RCDT-XBCT"
-            "C-203" -> "TRAD-CLNC-OTHR"
-            "D-203" -> "TRAD-CLNC-OTHR"
-            "C-204" -> "TRAD-DCCT-OTHR"
-            "D-204" -> "TRAD-DCCT-OTHR"
-            "C-205" -> "TRAD-GUAR-OTHR"
-            "D-205" -> "TRAD-GUAR-OTHR"
-            "C-206" -> "PMNT-RCDT-XBCT"
-            "C-208" -> "TRAD-MCOP-OTHR"
-            "D-208" -> "TRAD-MDOP-OTHR"
-            "D-209" -> "PMNT-ICHQ-XBCQ"
-            "D-210" -> "PMNT-ICDT-XBCT"
-            "C-211" -> "PMNT-RCDT-XBCT"
-            "D-212" -> "PMNT-ICDT-XBST"
-            "D-213" -> "PMNT-RDDT-XBDD"
-            "D-214" -> "TRAD-DOCC-OTHR"
-            "C-215" -> "TRAD-DOCC-OTHR"
-            "D-216" -> "PMNT-DRFT-STAM"
-            "C-217" -> "PMNT-DRFT-STAM"
-            // Alternative:
-            // "C-217" -> "PMNT-DRFT-STLR"
-            "D-218" -> "TRAD-DCCT-OTHR"
-            "C-219" -> "TRAD-DCCT-OTHR"
-            "C-220" -> "PMNT-RCHQ-XRCQ"
-            "C-221" -> "PMNT-RCHQ-XBCQ"
-            "D-222" -> "PMNT-ICHQ-XBCQ"
-            "D-223" -> "PMNT-ICHQ-XBCQ"
-            "C-224" -> "PMNT-CNTR-FCDP"
-            "D-225" -> "PMNT-CNTR-FCWD"
-            "C-301" -> "SECU-CUST-REDM"
-            "C-302" -> "SECU-CUST-DVCA"
-            "C-303" -> "SECU-SETT-TRAD"
-            "D-303" -> "SECU-SETT-TRAD"
-            "C-304" -> "SECU-OTHR-OTHR"
-            "D-304" -> "SECU-OTHR-OTHR"
-            "D-305" -> "SECU-OTHR-OTHR"
-            "D-306" -> "SECU-OTHR-OTHR"
-            "D-307" -> "SECU-SETT-SUBS"
-            "C-308" -> "SECU-CORP-EXWA"
-            "D-308" -> "SECU-CORP-EXWA"
-            "C-309" -> "SECU-CORP-BONU"
-            "D-309" -> "SECU-CORP-BONU"
-            "C-310" -> "SECU-MCOP-OTHR"
-            "D-310" -> "SECU-MDOP-OTHR"
-            "C-311" -> "DERV-OTHR-OTHR"
-            "D-311" -> "DERV-OTHR-OTHR"
-            "D-320" -> "SECU-CASH-TRFE"
-            "D-321" -> "SECU-CUST-CHRG"
-            "C-321" -> "SECU-CUST-CHRG"
-            "C-330" -> "SECU-CUST-INTR"
-            "C-340" -> "SECU-CUST-REDM"
-            "C-399" -> "ACMT-ACOP-PSTE"
-            "D-399" -> "ACMT-ADOP-PSTE"
-            "C-401" -> "FORX-SPOT-OTHR"
-            "D-401" -> "FORX-SPOT-OTHR"
-            "C-402" -> "FORX-FWRD-OTHR"
-            "D-402" -> "FORX-FWRD-OTHR"
-            "D-403" -> "FORX-MDOP-OTHR"
-            "D-404" -> "FORX-OTHR-OTHR"
-            "D-405" -> "FORX-OTHR-OTHR"
-            "C-406" -> "FORX-SPOT-OTHR"
-            "D-406" -> "FORX-SPOT-OTHR"
-            "C-407" -> "FORX-OTHR-OTHR"
-            "D-407" -> "FORX-OTHR-OTHR"
-            "C-408" -> "FORX-OTHR-OTHR"
-            "C-409" -> "FORX-OTHR-OTHR"
-            "D-411" -> "FORX-SPOT-OTHR"
-            "C-412" -> "FORX-SPOT-OTHR"
-            "D-413" -> "FORX-FWRD-OTHR"
-            "C-414" -> "FORX-FWRD-OTHR"
-            "D-415" -> "FORX-OTHR-OTHR"
-            "C-416" -> "FORX-OTHR-OTHR"
-            "D-417" -> "FORX-OTHR-OTHR"
-            "C-418" -> "FORX-OTHR-OTHR"
-            "D-419" -> "FORX-OTHR-OTHR"
-            "C-420" -> "FORX-OTHR-OTHR"
-            "C-421" -> "FORX-OTHR-OTHR"
-            "D-421" -> "FORX-OTHR-OTHR"
-            "C-422" -> "FORX-SWAP-OTHR"
-            "D-422" -> "FORX-SWAP-OTHR"
-            "C-423" -> "PMET-SPOT-OTHR"
-            "D-424" -> "PMET-SPOT-OTHR"
-            "D-601" -> "LDAS-FTLN-OTHR"
-            "C-602" -> "LDAS-FTLN-OTHR"
-            "D-603" -> "LDAS-FTLN-PPAY"
-            "D-604" -> "LDAS-MDOP-INTR"
-            "D-605" -> "LDAS-MDOP-INTR"
-            "C-606" -> "LDAS-FTLN-DDWN"
-            "D-606" -> "LDAS-FTLN-DDWN"
-            "D-607" -> "LDAS-OTHR-OTHR"
-            "D-801" -> "ACMT-MDOP-CHRG"
-            "D-802" -> "ACMT-MDOP-CHRG"
-            "D-803" -> "SECU-CUST-CHRG"
-            "D-804" -> "PMNT-MDOP-CHRG"
-            "C-804" -> "PMNT-MCOP-CHRG"
-            "C-805" -> "ACMT-OPCL-ACCC"
-            "D-805" -> "ACMT-OPCL-ACCC"
-            "C-806" -> "ACMT-MCOP-CHRG"
-            "D-806" -> "ACMT-MDOP-CHRG"
-            "C-807" -> "ACMT-MCOP-CHRG"
-            "D-807" -> "ACMT-MDOP-CHRG"
-            "C-808" -> "PMNT-MCOP-CHRG"
-            "D-808" -> "PMNT-MDOP-CHRG"
-            // Alternatives:
-            // "C-808" -> "TRAD-MCOP-CHRG"
-            // "D-808" -> "TRAD-MDOP-CHRG"
-            // "C-808" -> "ACMT-MCOP-CHRG"
-            // "D-808" -> "ACMT-MDOP-CHRG"
-            "D-809" -> "PMNT-MDOP-COMM"
-            "C-809" -> "PMNT-MCOP-COMM"
-            // Alternatives:
-            // "D-809" -> "ACMT-MDOP-COMM"
-            // "C-809" -> "ACMT-MCOP-COMM"
-            // "D-809" -> "TRAD-MDOP-COMM"
-            // "C-809" -> "TRAD-MCOP-COMM"
-            // "D-809" -> "LDAS-MDOP-COMM"
-            // "C-809" -> "LDAS-MCOP-COMM"
-            "D-810" -> "ACMT-MDOP-CHRG"
-            "C-810" -> "ACMT-MCOP-CHRG"
-            "D-811" -> "LDAS-MDOP-CHRG"
-            "C-811" -> "LDAS-MCOP-CHRG"
-            "D-812" -> "LDAS-MDOP-INTR"
-            "C-812" -> "LDAS-MCOP-INTR"
-            "D-813" -> "LDAS-MDOP-INTR"
-            "C-814" -> "ACMT-MCOP-INTR"
-            "D-814" -> "ACMT-MDOP-INTR"
-            "C-815" -> "ACMT-OTHR-OTHR"
-            "C-816" -> "ACMT-OTHR-OTHR"
-            "C-817" -> "ACMT-OTHR-OTHR"
-            "D-818" -> "PMNT-OTHR-OTHR"
-            "C-819" -> "PMNT-OTHR-OTHR"
-            "C-820" -> "PMNT-RCDT-BOOK"
-            "D-820" -> "PMNT-ICDT-BOOK"
-            "D-821" -> "PMNT-OTHR-OTHR"
-            "C-822" -> "PMNT-OTHR-OTHR"
-            "C-823" -> "LDAS-FTDP-RPMT"
-            "D-823" -> "LDAS-FTDP-DPST"
-            "D-824" -> "LDAS-OTHR-OTHR"
-            "D-825" -> "LDAS-OTHR-OTHR"
-            "D-826" -> "LDAS-OTHR-OTHR"
-            "D-827" -> "LDAS-OTHR-OTHR"
-            "C-828" -> "LDAS-FTDP-RPMT"
-            "D-828" -> "LDAS-FTDP-DPST"
-            "C-829" -> "LDAS-FTDP-RPMT"
-            "D-829" -> "LDAS-FTDP-DPST"
-            "C-830" -> "LDAS-FTDP-INTR"
-            "D-831" -> "XTND-NTAV-NTAV"
-            "D-832" -> "LDAS-OTHR-OTHR"
-            "C-833" -> "CAMT-ACCB-OTHR"
-            "D-833" -> "CAMT-ACCB-OTHR"
-            "C-834" -> "CAMT-ACCB-OTHR"
-            "D-834" -> "CAMT-ACCB-OTHR"
-            "C-835" -> "XTND-NTAV-NTAV"
-            "D-835" -> "XTND-NTAV-NTAV"
-            "C-836" -> "ACMT-MCOP-ADJT"
-            "D-836" -> "ACMT-MDOP-ADJT"
-            "D-837" -> "ACMT-MDOP-TAXE"
-            "C-888" -> "XTND-NTAV-NTAV"
-            "D-888" -> "XTND-NTAV-NTAV"
-            "C-899" -> "ACMT-ACOP-PSTE"
-            "D-899" -> "ACMT-ADOP-PSTE"
-            "D-997" -> "XTND-NTAV-NTAV"
-            "C-999" -> "XTND-NTAV-NTAV"
-            "D-999" -> "XTND-NTAV-NTAV"
-            else -> "XTND-NTAV-NTAV"
-        }
-    }
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
deleted file mode 100644
index 128b7a1b..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
+++ /dev/null
@@ -1,1023 +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/>
- */
-
-/**
- * Parse and generate ISO 20022 messages
- */
-package tech.libeufin.nexus.iso20022
-
-import AgentIdentification
-import Batch
-import BatchTransaction
-import CamtBankAccountEntry
-import CashAccount
-import CreditDebitIndicator
-import CurrencyAmount
-import CurrencyExchange
-import EntryStatus
-import GenericId
-import OrganizationIdentification
-import PartyIdentification
-import PostalAddress
-import PrivateIdentification
-import ReturnInfo
-import TransactionDetails
-import com.fasterxml.jackson.annotation.JsonInclude
-import com.fasterxml.jackson.annotation.JsonValue
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.http.*
-import io.ktor.util.reflect.*
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.w3c.dom.Document
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.bankaccount.IngestedTransactionsCount
-import tech.libeufin.nexus.bankaccount.findDuplicate
-import tech.libeufin.nexus.server.EbicsDialects
-import tech.libeufin.nexus.server.FetchLevel
-import tech.libeufin.nexus.server.PaymentUidQualifiers
-import tech.libeufin.util.*
-import toPlainString
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.ZoneId
-import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
-import tech.libeufin.nexus.logger
-
-
-enum class CashManagementResponseType(@get:JsonValue val jsonName: String) {
-    Report("report"),
-    Statement("statement"),
-    Notification("notification")
-}
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class CamtReport(
-    val id: String,
-    val creationDateTime: String?,
-    val legalSequenceNumber: Int?,
-    val electronicSequenceNumber: Int?,
-    val fromDate: String?,
-    val toDate: String?,
-    val reportingSource: String?,
-    val proprietaryReportingSource: String?,
-    val account: CashAccount,
-    val balances: List<Balance>,
-    val entries: List<CamtBankAccountEntry>
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class Balance(
-    val type: String?,
-    val subtype: String?,
-    val proprietaryType: String?,
-    val proprietarySubtype: String?,
-    val date: String,
-    val creditDebitIndicator: CreditDebitIndicator,
-    val amount: CurrencyAmount
-)
-
-data class CamtParseResult(
-    /**
-     * Message type in form of the ISO 20022 message name.
-     */
-    val messageType: CashManagementResponseType,
-    val messageId: String,
-    val creationDateTime: String,
-    /**
-     * One Camt document can contain multiple reports/statements
-     * for each account being owned by the requester.
-     */
-    val reports: List<CamtReport>
-)
-
-class CamtParsingError(msg: String) : Exception(msg)
-
-/**
- * Data that the LibEuFin nexus uses for payment initiation.
- * Subset of what ISO 20022 allows.
- */
-data class NexusPaymentInitiationData(
-    val debtorIban: String,
-    val debtorBic: String,
-    val debtorName: String,
-    val messageId: String,
-    val paymentInformationId: String,
-    val endToEndId: String? = null,
-    val amount: String,
-    val currency: String,
-    val subject: String,
-    val preparationTimestamp: Long,
-    val creditorName: String,
-    val creditorIban: String,
-    val creditorBic: String? = null,
-    val instructionId: String? = null
-)
-
-data class Pain001Namespaces(
-    val fullNamespace: String,
-    val xsdFilename: String
-)
-
-fun XmlElementBuilder.setBicAfterDialect(dialect: String?, bic: String) {
-    if (dialect == EbicsDialects.POSTFINANCE.dialectName)
-        element("BICFI") {
-            text(bic)
-        }
-    else element("BIC") {
-        text(bic)
-    }
-}
-/**
- * Create a PAIN.001 XML document according to the input data.
- * Needs to be called within a transaction block.
- */
-fun createPain001document(
-    paymentData: NexusPaymentInitiationData,
-    dialect: String? = null
-): String {
-
-    val namespace: Pain001Namespaces = if (dialect == "pf")
-        // The 2019 version of pain.001.
-        Pain001Namespaces(
-            fullNamespace = "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09",
-            xsdFilename = "pain.001.001.09.ch.03.xsd"
-        )
-    else Pain001Namespaces(
-        fullNamespace = "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03",
-        xsdFilename = "pain.001.001.03.xsd"
-    )
-
-    val paymentMethod = if (dialect == "pf")
-        "SDVA" else "SEPA"
-
-    val s = constructXml(indent = true) {
-        root("Document") {
-            attribute("xmlns", namespace.fullNamespace)
-            attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance";)
-            attribute(
-                "xsi:schemaLocation",
-                "${namespace.fullNamespace} ${namespace.xsdFilename}"
-            )
-            element("CstmrCdtTrfInitn") {
-                element("GrpHdr") {
-                    element("MsgId") {
-                        text(paymentData.messageId)
-                    }
-                    element("CreDtTm") {
-                        val dateMillis = paymentData.preparationTimestamp
-                        val dateFormatter = 
DateTimeFormatter.ISO_OFFSET_DATE_TIME
-                        val instant = Instant.ofEpochSecond(dateMillis / 1000)
-                        val zoned = ZonedDateTime.ofInstant(instant, 
ZoneId.systemDefault())
-                        text(dateFormatter.format(zoned))
-                    }
-                    element("NbOfTxs") {
-                        text("1")
-                    }
-                    element("CtrlSum") {
-                        text(paymentData.amount)
-                    }
-                    element("InitgPty/Nm") {
-                        text(paymentData.debtorName)
-                    }
-                }
-                element("PmtInf") {
-                    element("PmtInfId") {
-                        text(paymentData.paymentInformationId)
-                    }
-                    element("PmtMtd") {
-                        text("TRF")
-                    }
-                    element("BtchBookg") {
-                        text("true")
-                    }
-                    element("NbOfTxs") {
-                        text("1")
-                    }
-                    element("CtrlSum") {
-                        text(paymentData.amount)
-                    }
-                    element("PmtTpInf/SvcLvl/Cd") {
-                        text(paymentMethod)
-                    }
-                    element("ReqdExctnDt") {
-                        val dateMillis = paymentData.preparationTimestamp
-                        if (dialect == EbicsDialects.POSTFINANCE.dialectName)
-                            element("Dt") {
-                                
text(importDateFromMillis(dateMillis).toDashedDate())
-                            }
-                        else
-                            
text(importDateFromMillis(dateMillis).toDashedDate())
-                    }
-                    element("Dbtr/Nm") {
-                        text(paymentData.debtorName)
-                    }
-                    element("DbtrAcct/Id/IBAN") {
-                        text(paymentData.debtorIban)
-                    }
-                    element("DbtrAgt/FinInstnId") {
-                        setBicAfterDialect(dialect, paymentData.debtorBic)
-                    }
-                    element("ChrgBr") {
-                        text("SLEV")
-                    }
-                    element("CdtTrfTxInf") {
-                        element("PmtId") {
-                            paymentData.instructionId?.let {
-                                element("InstrId") { text(it) }
-                            }
-                            when (val eeid = paymentData.endToEndId) {
-                                null -> element("EndToEndId") { 
text("NOTPROVIDED") }
-                                else -> element("EndToEndId") { text(eeid) }
-                            }
-                        }
-                        element("Amt/InstdAmt") {
-                            attribute("Ccy", paymentData.currency)
-                            text(paymentData.amount)
-                        }
-                        val creditorBic = paymentData.creditorBic
-                        if (creditorBic != null) {
-                            element("CdtrAgt/FinInstnId") {
-                                setBicAfterDialect(dialect, creditorBic)
-                            }
-                        }
-                        element("Cdtr/Nm") {
-                            text(paymentData.creditorName)
-                        }
-                        element("CdtrAcct/Id/IBAN") {
-                            text(paymentData.creditorIban)
-                        }
-                        element("RmtInf/Ustrd") {
-                            text(paymentData.subject)
-                        }
-                    }
-                }
-            }
-        }
-    }
-    return s
-}
-
-private fun XmlElementDestructor.extractDateOrDateTime(): String {
-    return requireOnlyChild {
-        when (focusElement.localName) {
-            "Dt" -> focusElement.textContent
-            "DtTm" -> focusElement.textContent
-            else -> throw Exception("Invalid date / time: 
${focusElement.localName}")
-        }
-    }
-}
-
-private fun XmlElementDestructor.extractInnerPostalAddress(): PostalAddress {
-    return PostalAddress(
-        addressCode = maybeUniqueChildNamed("AdrTp") { 
maybeUniqueChildNamed("Cd") { focusElement.textContent } },
-        addressProprietaryIssuer = maybeUniqueChildNamed("AdrTp") {
-            maybeUniqueChildNamed("Prtry") {
-                maybeUniqueChildNamed("Issr") { focusElement.textContent }
-            }
-        },
-        addressProprietarySchemeName = maybeUniqueChildNamed("AdrTp") {
-            maybeUniqueChildNamed("Prtry") {
-                maybeUniqueChildNamed("SchmeNm") { focusElement.textContent }
-            }
-        },
-        addressProprietaryId = maybeUniqueChildNamed("AdrTp") {
-            maybeUniqueChildNamed("Prtry") {
-                maybeUniqueChildNamed("Id") { focusElement.textContent }
-            }
-        },
-        buildingName = maybeUniqueChildNamed("BldgNm") { 
focusElement.textContent },
-        buildingNumber = maybeUniqueChildNamed("BldgNb") { 
focusElement.textContent },
-        country = maybeUniqueChildNamed("Ctry") { focusElement.textContent },
-        countrySubDivision = maybeUniqueChildNamed("CtrySubDvsn") { 
focusElement.textContent },
-        department = maybeUniqueChildNamed("Dept") { focusElement.textContent 
},
-        districtName = maybeUniqueChildNamed("DstrctNm") { 
focusElement.textContent },
-        floor = maybeUniqueChildNamed("Flr") { focusElement.textContent },
-        postBox = maybeUniqueChildNamed("PstBx") { focusElement.textContent },
-        postCode = maybeUniqueChildNamed("PstCd") { focusElement.textContent },
-        room = maybeUniqueChildNamed("Room") { focusElement.textContent },
-        streetName = maybeUniqueChildNamed("StrtNm") { 
focusElement.textContent },
-        subDepartment = maybeUniqueChildNamed("SubDept") { 
focusElement.textContent },
-        townLocationName = maybeUniqueChildNamed("TwnLctnNm") { 
focusElement.textContent },
-        townName = maybeUniqueChildNamed("TwnNm") { focusElement.textContent },
-        addressLines = mapEachChildNamed("AdrLine") { focusElement.textContent 
}
-    )
-}
-
-private fun XmlElementDestructor.extractAgent(): AgentIdentification {
-    return AgentIdentification(
-        name = maybeUniqueChildNamed("FinInstnId") {
-            maybeUniqueChildNamed("Nm") { focusElement.textContent }
-        },
-        bic = requireUniqueChildNamed("FinInstnId") {
-            maybeUniqueChildNamed("BIC") { focusElement.textContent }
-        },
-        lei = requireUniqueChildNamed("FinInstnId") {
-            maybeUniqueChildNamed("LEI") { focusElement.textContent }
-        },
-        clearingSystemCode = requireUniqueChildNamed("FinInstnId") {
-            maybeUniqueChildNamed("ClrSysMmbId") {
-                maybeUniqueChildNamed("ClrSysId") {
-                    maybeUniqueChildNamed("Cd") { focusElement.textContent }
-                }
-            }
-        },
-        proprietaryClearingSystemCode = requireUniqueChildNamed("FinInstnId") {
-            maybeUniqueChildNamed("ClrSysMmbId") {
-                maybeUniqueChildNamed("ClrSysId") {
-                    maybeUniqueChildNamed("Prtry") { focusElement.textContent }
-                }
-            }
-        },
-        clearingSystemMemberId = requireUniqueChildNamed("FinInstnId") {
-            maybeUniqueChildNamed("ClrSysMmbId") {
-                maybeUniqueChildNamed("MmbId") { focusElement.textContent }
-            }
-        },
-        otherId = requireUniqueChildNamed("FinInstnId") { 
maybeUniqueChildNamed("Othr") { extractGenericId() } },
-        postalAddress = requireUniqueChildNamed("FinInstnId") { 
maybeUniqueChildNamed("PstlAdr") { extractInnerPostalAddress() } }
-    )
-}
-
-private fun XmlElementDestructor.extractGenericId(): GenericId {
-    return GenericId(
-        id = requireUniqueChildNamed("Id") { focusElement.textContent },
-        schemeName = maybeUniqueChildNamed("SchmeNm") {
-            maybeUniqueChildNamed("Cd") { focusElement.textContent }
-        },
-        issuer = maybeUniqueChildNamed("Issr") { focusElement.textContent },
-        proprietarySchemeName = maybeUniqueChildNamed("SchmeNm") {
-            maybeUniqueChildNamed("Prtry") { focusElement.textContent }
-        }
-    )
-}
-
-private fun XmlElementDestructor.extractAccount(): CashAccount {
-    var iban: String? = null
-    var otherId: GenericId? = null
-    val currency: String? = maybeUniqueChildNamed("Ccy") { 
focusElement.textContent }
-    val name: String? = maybeUniqueChildNamed("Nm") { focusElement.textContent 
}
-    requireUniqueChildNamed("Id") {
-        requireOnlyChild {
-            when (focusElement.localName) {
-                "IBAN" -> {
-                    iban = focusElement.textContent
-                }
-                "Othr" -> {
-                    otherId = extractGenericId()
-                }
-                else -> throw Error("invalid account identification")
-            }
-        }
-    }
-    return CashAccount(name, currency, iban, otherId)
-}
-
-private fun XmlElementDestructor.extractParty(): PartyIdentification {
-    val otherId: GenericId? = maybeUniqueChildNamed("Id") {
-        (maybeUniqueChildNamed("PrvtId") { focusElement } ?: 
maybeUniqueChildNamed("OrgId") { focusElement })?.run {
-            maybeUniqueChildNamed("Othr") {
-                extractGenericId()
-            }
-        }
-    }
-
-    val privateId = maybeUniqueChildNamed("Id") {
-        maybeUniqueChildNamed("PrvtId") {
-            maybeUniqueChildNamed("DtAndPlcOfBirth") {
-                PrivateIdentification(
-                    birthDate = maybeUniqueChildNamed("BirthDt") { 
focusElement.textContent },
-                    cityOfBirth = maybeUniqueChildNamed("CityOfBirth") { 
focusElement.textContent },
-                    countryOfBirth = maybeUniqueChildNamed("CtryOfBirth") { 
focusElement.textContent },
-                    provinceOfBirth = maybeUniqueChildNamed("PrvcOfBirth") { 
focusElement.textContent }
-                )
-            }
-        }
-    }
-
-    val organizationId = maybeUniqueChildNamed("Id") {
-        maybeUniqueChildNamed("OrgId") {
-            OrganizationIdentification(
-                bic = maybeUniqueChildNamed("BICOrBEI") { 
focusElement.textContent }
-                    ?: maybeUniqueChildNamed("AnyBIC") { 
focusElement.textContent },
-                lei = maybeUniqueChildNamed("LEI") { focusElement.textContent }
-            )
-        }
-    }
-
-    return PartyIdentification(
-        name = maybeUniqueChildNamed("Nm") { focusElement.textContent },
-        otherId = otherId,
-        privateId = privateId,
-        organizationId = organizationId,
-        countryOfResidence = maybeUniqueChildNamed("CtryOfRes") { 
focusElement.textContent },
-        postalAddress = maybeUniqueChildNamed("PstlAdr") { 
extractInnerPostalAddress() }
-    )
-}
-
-private fun XmlElementDestructor.extractCurrencyAmount(): CurrencyAmount {
-    return CurrencyAmount(
-        value = requireUniqueChildNamed("Amt") { focusElement.textContent },
-        currency = requireUniqueChildNamed("Amt") { 
focusElement.getAttribute("Ccy") }
-    )
-}
-
-private fun XmlElementDestructor.maybeExtractCurrencyAmount(): CurrencyAmount? 
{
-    return maybeUniqueChildNamed("Amt") {
-        CurrencyAmount(
-            focusElement.getAttribute("Ccy"),
-            focusElement.textContent
-        )
-    }
-}
-
-private fun XmlElementDestructor.extractMaybeCurrencyExchange(): 
CurrencyExchange? {
-    return maybeUniqueChildNamed("CcyXchg") {
-        CurrencyExchange(
-            sourceCurrency = requireUniqueChildNamed("SrcCcy") { 
focusElement.textContent },
-            targetCurrency = requireUniqueChildNamed("TrgtCcy") { 
focusElement.textContent },
-            contractId = maybeUniqueChildNamed("CtrctId") { 
focusElement.textContent },
-            exchangeRate = requireUniqueChildNamed("XchgRate") { 
focusElement.textContent },
-            quotationDate = maybeUniqueChildNamed("QtnDt") { 
focusElement.textContent },
-            unitCurrency = maybeUniqueChildNamed("UnitCcy") { 
focusElement.textContent }
-        )
-    }
-}
-
-private fun XmlElementDestructor.extractBatches(
-    inheritableAmount: CurrencyAmount,
-    outerCreditDebitIndicator: CreditDebitIndicator,
-    acctSvcrRef: String
-): List<Batch> {
-    if (mapEachChildNamed("NtryDtls") {}.size != 1) throw CamtParsingError(
-        "This money movement (AcctSvcrRef: $acctSvcrRef) is not a singleton #0"
-    )
-    val txs = requireUniqueChildNamed("NtryDtls") {
-        if (mapEachChildNamed("TxDtls") {}.size != 1) {
-            throw CamtParsingError("This money movement (AcctSvcrRef: 
$acctSvcrRef) is not a singleton #1")
-        }
-         requireUniqueChildNamed("TxDtls") {
-            val details = extractTransactionDetails(outerCreditDebitIndicator)
-            mutableListOf(
-                BatchTransaction(
-                    inheritableAmount,
-                    outerCreditDebitIndicator,
-                    details
-                )
-            )
-        }
-    }
-    return mutableListOf(
-        Batch(messageId = null, paymentInformationId = null, batchTransactions 
= txs)
-    )
-}
-
-private fun XmlElementDestructor.maybeExtractCreditDebitIndicator(): 
CreditDebitIndicator? {
-    return maybeUniqueChildNamed("CdtDbtInd") { focusElement.textContent 
}?.let {
-        CreditDebitIndicator.valueOf(it)
-    }
-}
-
-private fun XmlElementDestructor.extractTransactionDetails(
-    outerCreditDebitIndicator: CreditDebitIndicator
-): TransactionDetails {
-    val instructedAmount = maybeUniqueChildNamed("AmtDtls") {
-        maybeUniqueChildNamed("InstdAmt") { extractCurrencyAmount() }
-    }
-
-    val creditDebitIndicator = maybeExtractCreditDebitIndicator() ?: 
outerCreditDebitIndicator
-    val currencyExchange = maybeUniqueChildNamed("AmtDtls") {
-        val cxCntrVal = maybeUniqueChildNamed("CntrValAmt") { 
extractMaybeCurrencyExchange() }
-        val cxTx = maybeUniqueChildNamed("TxAmt") { 
extractMaybeCurrencyExchange() }
-        val cxInstr = maybeUniqueChildNamed("InstdAmt") { 
extractMaybeCurrencyExchange() }
-        cxCntrVal ?: cxTx ?: cxInstr
-    }
-
-    return TransactionDetails(
-        instructedAmount = instructedAmount,
-        counterValueAmount = maybeUniqueChildNamed("AmtDtls") {
-            maybeUniqueChildNamed("CntrValAmt") { extractCurrencyAmount() }
-        },
-        currencyExchange = currencyExchange,
-        interBankSettlementAmount = null,
-        endToEndId = maybeUniqueChildNamed("Refs") {
-            maybeUniqueChildNamed("EndToEndId") { focusElement.textContent }
-        },
-        paymentInformationId = maybeUniqueChildNamed("Refs") {
-            maybeUniqueChildNamed("PmtInfId") { focusElement.textContent }
-        },
-        accountServicerRef = maybeUniqueChildNamed("Refs") {
-            maybeUniqueChildNamed("AcctSvcrRef") { focusElement.textContent }
-        },
-        unstructuredRemittanceInformation = maybeUniqueChildNamed("RmtInf") {
-            val chunks = mapEachChildNamed("Ustrd") { focusElement.textContent 
}
-            if (chunks.isEmpty()) {
-                null
-            } else {
-                chunks.joinToString(separator = "")
-            }
-        },
-        creditorAgent = maybeUniqueChildNamed("RltdAgts") { 
maybeUniqueChildNamed("CdtrAgt") { extractAgent() } },
-        debtorAgent = maybeUniqueChildNamed("RltdAgts") { 
maybeUniqueChildNamed("DbtrAgt") { extractAgent() } },
-        debtorAccount = maybeUniqueChildNamed("RltdPties") { 
maybeUniqueChildNamed("DbtrAcct") { extractAccount() } },
-        creditorAccount = maybeUniqueChildNamed("RltdPties") { 
maybeUniqueChildNamed("CdtrAcct") { extractAccount() } },
-        debtor = maybeUniqueChildNamed("RltdPties") { 
maybeUniqueChildNamed("Dbtr") { extractParty() } },
-        creditor = maybeUniqueChildNamed("RltdPties") { 
maybeUniqueChildNamed("Cdtr") { extractParty() } },
-        proprietaryPurpose = maybeUniqueChildNamed("Purp") { 
maybeUniqueChildNamed("Prtry") { focusElement.textContent } },
-        purpose = maybeUniqueChildNamed("Purp") { maybeUniqueChildNamed("Cd") 
{ focusElement.textContent } },
-        ultimateCreditor = maybeUniqueChildNamed("RltdPties") { 
maybeUniqueChildNamed("UltmtCdtr") { extractParty() } },
-        ultimateDebtor = maybeUniqueChildNamed("RltdPties") { 
maybeUniqueChildNamed("UltmtDbtr") { extractParty() } },
-        returnInfo = maybeUniqueChildNamed("RtrInf") {
-            ReturnInfo(
-                originalBankTransactionCode = 
maybeUniqueChildNamed("OrgnlBkTxCd") {
-                    extractInnerBkTxCd(
-                        when (creditDebitIndicator) {
-                            CreditDebitIndicator.DBIT -> 
CreditDebitIndicator.CRDT
-                            CreditDebitIndicator.CRDT -> 
CreditDebitIndicator.DBIT
-                        }
-                    )
-                },
-                originator = maybeUniqueChildNamed("Orgtr") { extractParty() },
-                reason = maybeUniqueChildNamed("Rsn") { 
maybeUniqueChildNamed("Cd") { focusElement.textContent } },
-                proprietaryReason = maybeUniqueChildNamed("Rsn") { 
maybeUniqueChildNamed("Prtry") { focusElement.textContent } },
-                additionalInfo = maybeUniqueChildNamed("AddtlInf") { 
focusElement.textContent }
-            )
-        }
-    )
-}
-
-private fun XmlElementDestructor.extractInnerBkTxCd(creditDebitIndicator: 
CreditDebitIndicator): String {
-
-    val domain = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Cd") { 
focusElement.textContent } }
-    val family = maybeUniqueChildNamed("Domn") {
-        maybeUniqueChildNamed("Fmly") {
-            maybeUniqueChildNamed("Cd") { focusElement.textContent }
-        }
-    }
-    val subfamily = maybeUniqueChildNamed("Domn") {
-        maybeUniqueChildNamed("Fmly") {
-            maybeUniqueChildNamed("SubFmlyCd") { focusElement.textContent }
-        }
-    }
-    val proprietaryCode = maybeUniqueChildNamed("Prtry") {
-        maybeUniqueChildNamed("Cd") { focusElement.textContent }
-    }
-    val proprietaryIssuer = maybeUniqueChildNamed("Prtry") {
-        maybeUniqueChildNamed("Issr") { focusElement.textContent }
-    }
-
-    if (domain != null && family != null && subfamily != null) {
-        return "$domain-$family-$subfamily"
-    }
-    if (proprietaryIssuer == "DK" && proprietaryCode != null) {
-        val components = proprietaryCode.split("+")
-        if (components.size == 1) {
-            return GbicRules.getBtcFromGvc(creditDebitIndicator, components[0])
-        } else {
-            return GbicRules.getBtcFromGvc(creditDebitIndicator, components[1])
-        }
-    }
-    // FIXME: log/raise this somewhere?
-    return "XTND-NTAV-NTAV"
-}
-
-private fun XmlElementDestructor.extractInnerTransactions(dialect: String? = 
null): CamtReport {
-    val account = requireUniqueChildNamed("Acct") { extractAccount() }
-
-    val balances = mapEachChildNamed("Bal") {
-        Balance(
-            type = maybeUniqueChildNamed("Tp") {
-                maybeUniqueChildNamed("CdOrPrtry") {
-                    maybeUniqueChildNamed("Cd") { focusElement.textContent }
-                }
-            },
-            proprietaryType = maybeUniqueChildNamed("Tp") {
-                maybeUniqueChildNamed("CdOrPrtry") {
-                    maybeUniqueChildNamed("Prtry") { focusElement.textContent }
-                }
-            },
-            date = requireUniqueChildNamed("Dt") { extractDateOrDateTime() },
-            creditDebitIndicator = requireUniqueChildNamed("CdtDbtInd") { 
focusElement.textContent }.let {
-                CreditDebitIndicator.valueOf(it)
-            },
-            subtype = maybeUniqueChildNamed("Tp") {
-                maybeUniqueChildNamed("SubTp") { maybeUniqueChildNamed("Cd") { 
focusElement.textContent } }
-            },
-            proprietarySubtype = maybeUniqueChildNamed("Tp") {
-                maybeUniqueChildNamed("SubTp") { 
maybeUniqueChildNamed("Prtry") { focusElement.textContent } }
-            },
-            amount = extractCurrencyAmount()
-        )
-    }
-    // Note: multiple Ntry's *are* allowed.  What is not allowed is
-    // multiple money transactions *within* one Ntry element.
-    val entries = mapEachChildNamed("Ntry") {
-        val amount = extractCurrencyAmount()
-        val status = requireUniqueChildNamed("Sts") {
-            val textContent = if (dialect == 
EbicsDialects.POSTFINANCE.dialectName) {
-                requireUniqueChildNamed("Cd") {
-                    focusElement.textContent
-                }
-            } else
-                focusElement.textContent
-            textContent.let {
-                EntryStatus.valueOf(it)
-            }
-        }
-        val creditDebitIndicator = requireUniqueChildNamed("CdtDbtInd") { 
focusElement.textContent }.let {
-            CreditDebitIndicator.valueOf(it)
-        }
-        val btc = requireUniqueChildNamed("BkTxCd") {
-            extractInnerBkTxCd(creditDebitIndicator)
-        }
-        val acctSvcrRef = maybeUniqueChildNamed("AcctSvcrRef") { 
focusElement.textContent }
-        val entryRef = maybeUniqueChildNamed("NtryRef") { 
focusElement.textContent }
-
-        val currencyExchange = maybeUniqueChildNamed("AmtDtls") {
-            val cxCntrVal = maybeUniqueChildNamed("CntrValAmt") { 
extractMaybeCurrencyExchange() }
-            val cxTx = maybeUniqueChildNamed("TxAmt") { 
extractMaybeCurrencyExchange() }
-            val cxInstr = maybeUniqueChildNamed("InstrAmt") { 
extractMaybeCurrencyExchange() }
-            cxCntrVal ?: cxTx ?: cxInstr
-        }
-
-        val counterValueAmount = maybeUniqueChildNamed("AmtDtls") {
-            maybeUniqueChildNamed("CntrValAmt") { extractCurrencyAmount() }
-        }
-
-        val instructedAmount = maybeUniqueChildNamed("AmtDtls") {
-            maybeUniqueChildNamed("InstdAmt") { extractCurrencyAmount() }
-        }
-
-        CamtBankAccountEntry(
-            amount = amount,
-            status = status,
-            currencyExchange = currencyExchange,
-            counterValueAmount = counterValueAmount,
-            instructedAmount = instructedAmount,
-            creditDebitIndicator = creditDebitIndicator,
-            bankTransactionCode = btc,
-            batches = extractBatches(
-                amount,
-                creditDebitIndicator,
-                acctSvcrRef ?: "AcctSvcrRef not given/found"),
-            bookingDate = maybeUniqueChildNamed("BookgDt") { 
extractDateOrDateTime() },
-            valueDate = maybeUniqueChildNamed("ValDt") { 
extractDateOrDateTime() },
-            accountServicerRef = acctSvcrRef,
-            entryRef = entryRef
-        )
-    }
-    return CamtReport(
-        account = account,
-        entries = entries,
-        creationDateTime = maybeUniqueChildNamed("CreDtTm") { 
focusElement.textContent },
-        balances = balances,
-        electronicSequenceNumber = maybeUniqueChildNamed("ElctrncSeqNb") { 
focusElement.textContent.toInt() },
-        legalSequenceNumber = maybeUniqueChildNamed("LglSeqNb") { 
focusElement.textContent.toInt() },
-        fromDate = maybeUniqueChildNamed("FrToDt") { 
maybeUniqueChildNamed("FrDtTm") { focusElement.textContent } },
-        toDate = maybeUniqueChildNamed("FrToDt") { 
maybeUniqueChildNamed("ToDtTm") { focusElement.textContent } },
-        id = requireUniqueChildNamed("Id") { focusElement.textContent },
-        proprietaryReportingSource = maybeUniqueChildNamed("RptgSrc") { 
maybeUniqueChildNamed("Prtry") { focusElement.textContent } },
-        reportingSource = maybeUniqueChildNamed("RptgSrc") { 
maybeUniqueChildNamed("Cd") { focusElement.textContent } }
-    )
-}
-
-/**
- * Extract a list of transactions from
- * an ISO20022 camt.052 / camt.053 message.
- */
-fun parseCamtMessage(doc: Document, dialect: String? = null): CamtParseResult {
-    return destructXml(doc) {
-        requireRootElement("Document") {
-            // Either bank to customer statement or report
-            val reports = requireOnlyChild {
-                when (focusElement.localName) {
-                    "BkToCstmrAcctRpt" -> {
-                        mapEachChildNamed("Rpt") {
-                            extractInnerTransactions(dialect)
-                        }
-                    }
-                    "BkToCstmrStmt" -> {
-                        mapEachChildNamed("Stmt") {
-                            extractInnerTransactions(dialect)
-                        }
-                    }
-                    "BkToCstmrDbtCdtNtfctn" -> {
-                        mapEachChildNamed("Ntfctn") {
-                            extractInnerTransactions(dialect)
-                        }
-                    }
-                    else -> {
-                        throw CamtParsingError("expected statement or report")
-                    }
-                }
-            }
-            val messageId = requireOnlyChild {
-                requireUniqueChildNamed("GrpHdr") {
-                    requireUniqueChildNamed("MsgId") { 
focusElement.textContent }
-                }
-            }
-            val creationDateTime = requireOnlyChild {
-                requireUniqueChildNamed("GrpHdr") {
-                    requireUniqueChildNamed("CreDtTm") { 
focusElement.textContent }
-                }
-            }
-            val messageType = requireOnlyChild {
-                when (focusElement.localName) {
-                    "BkToCstmrAcctRpt" -> CashManagementResponseType.Report
-                    "BkToCstmrStmt" -> CashManagementResponseType.Statement
-                    "BkToCstmrDbtCdtNtfctn" -> 
CashManagementResponseType.Notification
-                    else -> {
-                        throw CamtParsingError("expected statement or report")
-                    }
-                }
-            }
-            CamtParseResult(
-                reports = reports,
-                messageId = messageId,
-                messageType = messageType,
-                creationDateTime = creationDateTime
-            )
-        }
-    }
-}
-
-// Get timestamp in milliseconds, according to the EBICS+camt dialect.
-fun getTimestampInMillis(
-    dateTimeFromCamt: String,
-    dialect: String? = null
-): Long {
-    return when(dialect) {
-        EbicsDialects.POSTFINANCE.dialectName -> {
-            val withoutTimezone = LocalDateTime.parse(
-                dateTimeFromCamt,
-                DateTimeFormatter.ISO_LOCAL_DATE_TIME
-            )
-            ZonedDateTime.of(
-                withoutTimezone,
-                ZoneId.of("Europe/Zurich")).toInstant().toEpochMilli()
-        }
-        else -> {
-            ZonedDateTime.parse(
-                dateTimeFromCamt,
-                DateTimeFormatter.ISO_DATE_TIME
-            ).toInstant().toEpochMilli()
-        }
-    }
-}
-
-/**
- * Extracts the UID from the payment, according to dialect
- * and direction.  It returns the _qualified_ string from such
- * ID.  A qualified string has the format "$qualifier:$extracted_id".
- * $qualifier is a constant that gives more context about the
- * actual $extracted_id;  for example, it may indicate that the
- * ID was assigned by the bank, or by Nexus when it uploaded
- * the payment initiation in the first place.
- *
- * NOTE: this version _still_ expect only singleton transactions
- * in the input.  That means _only one_ element is expected at the
- * lowest level of the camt.05x report.  This may/should change in
- * future versions.
- */
-fun extractPaymentUidFromSingleton(
-    ntry: CamtBankAccountEntry,
-    camtMessageId: String, // used to print errors.
-    dialect: String?
-    ): String {
-    // First check if the input is a singleton.
-    val batchTransactions: List<BatchTransaction>? = 
ntry.batches?.get(0)?.batchTransactions
-    val tx: BatchTransaction = if (ntry.batches?.size != 1 || 
batchTransactions?.size != 1) {
-        logger.error("camt message ${camtMessageId} has non singleton 
transactions.")
-        throw internalServerError("Dialect $dialect sent camt with non 
singleton transactions.")
-    } else
-        batchTransactions[0]
-
-    when(dialect) {
-        EbicsDialects.POSTFINANCE.dialectName -> {
-            if (tx.creditDebitIndicator == CreditDebitIndicator.DBIT) {
-                val expectedEndToEndId = tx.details.endToEndId
-                /**
-                 * Because this is an outgoing transaction, and because
-                 * Nexus should have included the EndToEndId in the original
-                 * pain.001, this transaction must have it (recall: EndToEndId
-                 * is mandatory in the pain.001).  A null value means therefore
-                 * that the payment was done via another mean than pain.001.
-                 */
-                if (expectedEndToEndId == null) {
-                    logger.error("Camt '$camtMessageId' shows outgoing payment 
_without_ the EndToEndId." +
-                            "  This likely wasn't initiated via pain.001"
-                    )
-                    throw internalServerError("Internal reconciliation error 
(no EndToEndId)")
-                }
-                return "${PaymentUidQualifiers.USER_GIVEN}:$expectedEndToEndId"
-            }
-            // Didn't return/throw before, it must be an incoming payment.
-            val maybeAcctSvcrRef = tx.details.accountServicerRef
-            // Expecting this value to be at the lowest level, as observed on 
the test platform.
-            val expectedAcctSvcrRef = tx.details.accountServicerRef
-            if (expectedAcctSvcrRef == null) {
-                logger.error("AcctSvcrRef was expected at the lowest tx level 
for dialect: $dialect, but wasn't found")
-                throw internalServerError("Internal reconciliation error (no 
AcctSvcrRef at lowest tx level)")
-            }
-            return "${PaymentUidQualifiers.BANK_GIVEN}:$expectedAcctSvcrRef"
-        }
-        // This is the default dialect, the one tested with GLS.
-        null -> {
-            /**
-             * This dialect has shown the AcctSvcrRef to be always given
-             * at the level that _contains_ the (singleton) transaction(s).
-             * This occurs _regardless_ of the payment direction.
-             */
-            val expectedAcctSvcrRef = ntry.accountServicerRef
-            if (expectedAcctSvcrRef == null) {
-                logger.error("AcctSvcrRef was expected at the outer tx level 
for dialect: GLS, but wasn't found.")
-                throw internalServerError("Internal reconciliation error: 
AcctSvcrRef not found at outer level.")
-            }
-            return "${PaymentUidQualifiers.BANK_GIVEN}:$expectedAcctSvcrRef"
-        }
-        else -> throw internalServerError("Dialect $dialect is not supported.")
-    }
-}
-
-/**
- * Given that every CaMt is a collection of reports/statements
- * where each of them carries the bank account balance and a list
- * of transactions, this function:
- *
- * - extracts the balance (storing a NexusBankBalanceEntity)
- * - updates timestamps in NexusBankAccountEntity to the last seen
- *   report/statement.
- * - finds which transactions were already downloaded.
- * - stores a new NexusBankTransactionEntity for each new tx
- *   accounted in the report/statement.
- * - tries to link the new transaction with a submitted one, in
- *   case of DBIT transaction.
- * - returns a IngestedTransactionCount object.
- */
-fun ingestCamtMessageIntoAccount(
-    bankAccountId: String,
-    camtDoc: Document,
-    fetchLevel: FetchLevel,
-    dialect: String? = null
-): IngestedTransactionsCount {
-    /**
-     * Ensure that the level is not ALL, as the parser expects
-     * the exact type for the one message being parsed.
-     */
-    if (fetchLevel == FetchLevel.ALL)
-        throw internalServerError("Parser needs exact camt type (ALL not 
permitted).")
-
-    var newTransactions = 0
-    var downloadedTransactions = 0
-    transaction {
-        val acct = NexusBankAccountEntity.findByName(bankAccountId)
-        if (acct == null) {
-            throw NexusError(HttpStatusCode.NotFound, "user not found")
-        }
-        val res = try { parseCamtMessage(camtDoc, dialect) } catch (e: 
CamtParsingError) {
-            logger.warn("Invalid CAMT received from bank: ${e.message}")
-            newTransactions = -1
-            return@transaction
-        }
-        res.reports.forEach {
-            NexusAssert(
-                it.account.iban == acct.iban,
-                "Nexus hit a report or statement of a wrong IBAN!"
-            )
-            it.balances.forEach { b ->
-                if (b.type == "CLBD") {
-                    val lastBalance = NexusBankBalanceEntity.all().lastOrNull()
-                    /**
-                     * Store balances different from the one that came from 
the bank,
-                     * or the very first balance.  This approach has the 
following inconvenience:
-                     * the 'balance' held at Nexus does not differentiate 
between one
-                     * coming from a statement and one coming from a report.  
As a consequence,
-                     * the two types of balances may override each other 
without notice.
-                     */
-                    if ((lastBalance == null) ||
-                        (b.amount.toPlainString() != lastBalance.balance)) {
-                        NexusBankBalanceEntity.new {
-                            bankAccount = acct
-                            balance = b.amount.toPlainString()
-                            creditDebitIndicator = b.creditDebitIndicator.name
-                            date = b.date
-                        }
-                    }
-                }
-            }
-        }
-        // Updating the local bank account state timestamps according to the 
current document.
-        val stamp = getTimestampInMillis(res.creationDateTime, dialect = 
dialect)
-        when (fetchLevel) {
-            FetchLevel.REPORT -> {
-                val s = acct.lastReportCreationTimestamp
-                if (s == null || stamp > s) {
-                    acct.lastReportCreationTimestamp = stamp
-                }
-            }
-            FetchLevel.STATEMENT -> {
-                val s = acct.lastStatementCreationTimestamp
-                if (s == null || stamp > s) {
-                    acct.lastStatementCreationTimestamp = stamp
-                }
-            }
-            FetchLevel.NOTIFICATION -> {
-                val s = acct.lastNotificationCreationTimestamp
-                if (s == null || stamp > s) {
-                    acct.lastNotificationCreationTimestamp = stamp
-                }
-            }
-            // Silencing the compiler: the 'ALL' case was checked at the top 
of this function.
-            else -> {}
-        }
-        val entries: List<CamtBankAccountEntry> = res.reports.map { it.entries 
}.flatten()
-        var newPaymentsLog = ""
-        downloadedTransactions = entries.size
-        txloop@ for (entry: CamtBankAccountEntry in entries) {
-            val singletonBatchedTransaction: BatchTransaction = 
entry.batches?.get(0)?.batchTransactions?.get(0)
-                ?: throw NexusError(
-                    HttpStatusCode.InternalServerError,
-                    "Singleton money movements policy wasn't respected"
-                )
-            if (entry.status != EntryStatus.BOOK) {
-                logger.info("camt message '${res.messageId}' has a " +
-                        "non-BOOK transaction, ignoring it."
-                )
-                continue
-            }
-            val paymentUid = extractPaymentUidFromSingleton(
-                ntry = entry,
-                camtMessageId = res.messageId,
-                dialect = dialect
-            )
-            val duplicate = findDuplicate(bankAccountId, paymentUid)
-            if (duplicate != null) {
-                logger.info("Found a duplicate, UID is $paymentUid")
-                // https://bugs.gnunet.org/view.php?id=6381
-                continue@txloop
-            }
-            /* Checking for the unstructured remittance information before
-               storing the payment in the database.  */
-            val paymentSubject = entry.getSingletonSubject() // throws if not 
found.
-            val rawEntity = NexusBankTransactionEntity.new {
-                bankAccount = acct
-                accountTransactionId = paymentUid
-                amount = singletonBatchedTransaction.amount.value
-                currency = singletonBatchedTransaction.amount.currency
-                transactionJson = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(entry)
-                creditDebitIndicator = 
singletonBatchedTransaction.creditDebitIndicator.name
-                status = entry.status
-            }
-            rawEntity.flush()
-            newTransactions++
-            newPaymentsLog += "\n- $paymentSubject"
-
-            // This block tries to acknowledge a former outgoing payment as 
booked.
-            if (singletonBatchedTransaction.creditDebitIndicator == 
CreditDebitIndicator.DBIT) {
-                val t0 = singletonBatchedTransaction.details
-                val endToEndId = t0.endToEndId
-                if (endToEndId != null) {
-                    logger.debug("Reconciling outgoing payment with 
EndToEndId: $endToEndId")
-                    val paymentInitiation = PaymentInitiationEntity.find {
-                        PaymentInitiationsTable.bankAccount eq acct.id and (
-                                // pmtInfId is a value that the payment 
submitter
-                                // asked the bank to associate with the 
payment to be made.
-                                PaymentInitiationsTable.endToEndId eq 
endToEndId)
-
-                    }.firstOrNull()
-                    if (paymentInitiation != null) {
-                        logger.info("Could confirm one initiated payment: 
$endToEndId")
-                        paymentInitiation.confirmationTransaction = rawEntity
-                    }
-                }
-                // Every payment initiated by Nexus has EndToEndId.  Warn if 
not found.
-                else
-                    logger.warn("Camt ${res.messageId} has outgoing payment 
without EndToEndId..")
-            }
-        }
-        if (newTransactions > 0)
-            logger.debug("Camt $fetchLevel '${res.messageId}' has new 
payments:${newPaymentsLog}")
-    }
-
-    return IngestedTransactionsCount(
-        newTransactions = newTransactions,
-        downloadedTransactions = downloadedTransactions
-    )
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
deleted file mode 100644
index 175509cd..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package tech.libeufin.nexus.server
-
-import io.ktor.http.*
-import tech.libeufin.nexus.*
-import tech.libeufin.util.internalServerError
-
-// Type holding parameters of GET /transactions.
-data class GetTransactionsParams(
-    val bankAccountId: String,
-    val startIndex: Long,
-    val resultSize: Long
-)
-
-fun unknownBankAccount(bankAccountLabel: String): NexusError {
-    return NexusError(
-        HttpStatusCode.NotFound,
-        "Bank account $bankAccountLabel was not found"
-    )
-}
-
-/**
- * FIXME:
- * enum type names were introduced after 0.9.2 and need to
- * be employed wherever now type names are passed as plain
- * strings.
- */
-enum class EbicsDialects(val dialectName: String) {
-    POSTFINANCE("pf")
-}
-
-/**
- * Nexus needs to uniquely identify a payment, in order
- * to spot the same payment to be ingested more than once.
- * For example, payment X may have been already ingested
- * (and possibly led to a Taler withdrawal) via a EBICS C52
- * order, and might be later again downloaded via another
- * EBICS order (e.g. C53).  The second time this payment
- * reaches Nexus, it must NOT be considered new, therefore
- * Nexus needs a UID to check its database for the presence
- * of known payments.  Every bank assigns UIDs in a different
- * fashion, sometimes even differentiating between incoming and
- * outgoing payments; Nexus therefore classifies those UIDs
- * by assigning them one of the names defined in the following
- * enum class.  This way, Nexus has more control when it tries
- * to locally reconcile payments.
- */
-enum class PaymentUidQualifiers {
-    BANK_GIVEN,
-    USER_GIVEN
-}
-
-// Valid connection types.
-enum class BankConnectionType(val typeName: String) {
-    EBICS("ebics"),
-    X_LIBEUFIN_BANK("x-libeufin-bank");
-    companion object {
-        /**
-         * This method takes legacy bank connection type names as input
-         * and _tries_ to return the correspondent enum type.  This
-         * fixes the cases where bank connection types are passed as
-         * easy-to-break arbitrary strings; eventually this method should
-         * be discarded and only enum types be passed as connection type names.
-         */
-        fun parseBankConnectionType(typeName: String): BankConnectionType {
-            return when(typeName) {
-                "ebics" -> EBICS
-                "x-libeufin-bank" -> X_LIBEUFIN_BANK
-                else -> throw internalServerError(
-                    "Cannot extract ${this::class.java.typeName}' instance 
from name: $typeName'"
-                )
-            }
-        }
-    }
-}
-// Valid facade types
-enum class NexusFacadeType(val facadeType: String) {
-    TALER("taler-wire-gateway"),
-    ANASTASIS("anastasis")
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
deleted file mode 100644
index 39955778..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
+++ /dev/null
@@ -1,444 +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.nexus.server
-
-import CamtBankAccountEntry
-import EntryStatus
-import com.fasterxml.jackson.annotation.JsonSubTypes
-import com.fasterxml.jackson.annotation.JsonTypeInfo
-import com.fasterxml.jackson.annotation.JsonTypeName
-import com.fasterxml.jackson.annotation.JsonValue
-import com.fasterxml.jackson.databind.JsonNode
-import tech.libeufin.util.*
-import java.time.Instant
-import java.time.ZoneId
-import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
-import java.time.format.DateTimeFormatterBuilder
-import java.time.temporal.ChronoField
-
-
-data class BackupRequestJson(
-    val passphrase: String
-)
-
-@JsonTypeInfo(
-    use = JsonTypeInfo.Id.NAME,
-    include = JsonTypeInfo.As.PROPERTY,
-    property = "paramType"
-)
-@JsonSubTypes(
-    JsonSubTypes.Type(value = EbicsStandardOrderParamsDateJson::class, name = 
"standard-date-range"),
-    JsonSubTypes.Type(value = EbicsStandardOrderParamsEmptyJson::class, name = 
"standard-empty"),
-    JsonSubTypes.Type(value = EbicsGenericOrderParamsJson::class, name = 
"generic")
-)
-abstract class EbicsOrderParamsJson {
-    abstract fun toOrderParams(): EbicsOrderParams
-}
-
-@JsonTypeName("generic")
-class EbicsGenericOrderParamsJson(
-    val params: Map<String, String>
-) : EbicsOrderParamsJson() {
-    override fun toOrderParams(): EbicsOrderParams {
-        return EbicsGenericOrderParams(params)
-    }
-}
-
-@JsonTypeName("standard-empty")
-class EbicsStandardOrderParamsEmptyJson : EbicsOrderParamsJson() {
-    override fun toOrderParams(): EbicsOrderParams {
-        return EbicsStandardOrderParams(null)
-    }
-}
-
-object EbicsDateFormat {
-    var fmt = DateTimeFormatterBuilder()
-        .append(DateTimeFormatter.ISO_DATE)
-        .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
-        .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
-        .parseDefaulting(ChronoField.OFFSET_SECONDS, 
ZoneId.systemDefault().rules.getOffset(Instant.now()).totalSeconds.toLong())
-        .toFormatter()!!
-}
-
-@JsonTypeName("standard-date-range")
-class EbicsStandardOrderParamsDateJson(
-    private val start: String,
-    private val end: String
-) : EbicsOrderParamsJson() {
-    override fun toOrderParams(): EbicsOrderParams {
-        val dateRange =
-            EbicsDateRange(
-                ZonedDateTime.parse(this.start, EbicsDateFormat.fmt),
-                ZonedDateTime.parse(this.end, EbicsDateFormat.fmt)
-            )
-        return EbicsStandardOrderParams(dateRange)
-    }
-}
-
-data class NexusErrorDetailJson(
-    val type: String,
-    val description: String
-)
-data class NexusErrorJson(
-    val error: NexusErrorDetailJson
-)
-data class NexusMessage(
-    val message: String
-)
-
-data class ErrorResponse(
-    val code: Int,
-    val hint: String,
-    val detail: String,
-)
-
-data class BankConnectionInfo(
-    val name: String,
-    val type: String
-)
-
-data class BankConnectionsList(
-    val bankConnections: MutableList<BankConnectionInfo> = mutableListOf()
-)
-
-data class BankConnectionDeletion(
-    val bankConnectionId: String
-)
-
-data class EbicsHostTestRequest(
-    val ebicsBaseUrl: String,
-    val ebicsHostId: String
-)
-
-/**
- * This object is used twice: as a response to the backup request,
- * and as a request to the backup restore.  Note: in the second case
- * the client must provide the passphrase.
- */
-data class EbicsKeysBackupJson(
-    // Always "ebics"
-    val type: String,
-    val userID: String,
-    val partnerID: String,
-    val hostID: String,
-    val ebicsURL: String,
-    val authBlob: String,
-    val encBlob: String,
-    val sigBlob: String,
-    val bankAuthBlob: String?,
-    val bankEncBlob: String?,
-    val dialect: String? = null
-)
-
-enum class PermissionChangeAction(@get:JsonValue val jsonName: String) {
-    GRANT("grant"), REVOKE("revoke")
-}
-
-data class Permission(
-    val subjectType: String,
-    val subjectId: String,
-    val resourceType: String,
-    val resourceId: String,
-    val permissionName: String
-)
-
-data class PermissionQuery(
-    val resourceType: String,
-    val resourceId: String,
-    val permissionName: String,
-)
-
-data class ChangePermissionsRequest(
-    val action: PermissionChangeAction,
-    val permission: Permission
-)
-
-enum class FetchLevel(@get:JsonValue val jsonName: String) {
-    REPORT("report"),
-    STATEMENT("statement"),
-    NOTIFICATION("notification"),
-    /**
-     * Although not strictly used to get camt documents - typically
-     * gets pain.002 documents, this level still participates in downloading
-     * bank account activity, so placing it here.
-     */
-    RECEIPT("receipt"),
-    /**
-     * Uses of ALL do NOT include RECEIPT, in the current version.
-     */
-    ALL("all");
-}
-
-/**
- * Instructions on what range to fetch from the bank,
- * and which source(s) to use.
- *
- * Intended to be convenient to specify.
- */
-@JsonTypeInfo(
-    use = JsonTypeInfo.Id.NAME,
-    include = JsonTypeInfo.As.PROPERTY,
-    property = "rangeType"
-)
-@JsonSubTypes(
-    JsonSubTypes.Type(value = FetchSpecLatestJson::class, name = "latest"),
-    JsonSubTypes.Type(value = FetchSpecAllJson::class, name = "all"),
-    JsonSubTypes.Type(value = FetchSpecPreviousDaysJson::class, name = 
"previous-days"),
-    JsonSubTypes.Type(value = FetchSpecSinceLastJson::class, name = 
"since-last"),
-    JsonSubTypes.Type(value = FetchSpecTimeRangeJson::class, name = 
"time-range")
-)
-abstract class FetchSpecJson(
-    val level: FetchLevel,
-    val bankConnection: String?,
-    val start: String? = null,
-    val end: String? = null
-)
-
-@JsonTypeName("latest")
-class FetchSpecLatestJson(level: FetchLevel, bankConnection: String?) : 
FetchSpecJson(level, bankConnection)
-
-@JsonTypeName("all")
-class FetchSpecAllJson(level: FetchLevel, bankConnection: String?) : 
FetchSpecJson(level, bankConnection)
-
-@JsonTypeName("since-last")
-class FetchSpecSinceLastJson(level: FetchLevel, bankConnection: String?) : 
FetchSpecJson(level, bankConnection)
-
-@JsonTypeName("time-range")
-class FetchSpecTimeRangeJson(
-    level: FetchLevel,
-    start: String,
-    end: String,
-    bankConnection: String?
-) : FetchSpecJson(level, bankConnection, start, end)
-
-@JsonTypeName("previous-days")
-class FetchSpecPreviousDaysJson(level: FetchLevel, bankConnection: String?, 
val number: Int) :
-    FetchSpecJson(level, bankConnection)
-
-@JsonTypeInfo(
-    use = JsonTypeInfo.Id.NAME,
-    include = JsonTypeInfo.As.PROPERTY,
-    property = "source"
-)
-@JsonSubTypes(
-    JsonSubTypes.Type(value = 
CreateBankConnectionFromBackupRequestJson::class, name = "backup"),
-    JsonSubTypes.Type(value = CreateBankConnectionFromNewRequestJson::class, 
name = "new")
-)
-abstract class CreateBankConnectionRequestJson(
-    val name: String
-)
-
-@JsonTypeName("backup")
-class CreateBankConnectionFromBackupRequestJson(
-    name: String,
-    val passphrase: String?,
-    val data: JsonNode
-) : CreateBankConnectionRequestJson(name)
-
-@JsonTypeName("new")
-class CreateBankConnectionFromNewRequestJson(
-    name: String,
-    val type: String,
-    val dialect: String? = null,
-    val data: JsonNode
-) : CreateBankConnectionRequestJson(name)
-
-data class EbicsNewTransport(
-    val userID: String,
-    val partnerID: String,
-    val hostID: String,
-    val ebicsURL: String,
-    val systemID: String?,
-    val dialect: String? = null
-)
-
-/**
- * Credentials and URL to access Sandbox and talk JSON to it.
- * See 
https://docs.taler.net/design-documents/038-demobanks-protocol-suppliers.html#static-x-libeufin-bank-with-dynamic-demobank
- * for an introduction on x-libeufin-bank.
- */
-data class XLibeufinBankTransport(
-    val username: String,
-    val password: String,
-    val baseUrl: String
-)
-
-/** Response type of "GET /prepared-payments/{uuid}" */
-data class PaymentStatus(
-    val paymentInitiationId: String,
-    val submitted: Boolean,
-    val creditorIban: String,
-    val creditorBic: String?,
-    val creditorName: String,
-    val amount: String,
-    val subject: String,
-    val submissionDate: String?,
-    val preparationDate: String,
-    val status: EntryStatus?
-)
-
-data class Transactions(
-    val transactions: MutableList<CamtBankAccountEntry> = mutableListOf()
-)
-
-data class BankProtocolsResponse(
-    val protocols: List<String>
-)
-
-/** Request type of "POST /prepared-payments" */
-data class CreatePaymentInitiationRequest(
-    val iban: String,
-    val bic: String,
-    val name: String,
-    val amount: String,
-    val subject: String,
-    // When it's null, the client doesn't expect/need idempotence.
-    val uid: String? = null
-)
-
-/** Response type of "POST /prepared-payments" */
-data class PaymentInitiationResponse(
-    val uuid: String
-)
-
-/** Response type of "GET /user" */
-data class UserResponse(
-    val username: String,
-    val superuser: Boolean,
-)
-
-/** Request type of "POST /users" */
-data class CreateUserRequest(
-    val username: String,
-    val password: String
-)
-
-data class ChangeUserPassword(
-    val newPassword: String
-)
-
-data class UserInfo(
-    val username: String,
-    val superuser: Boolean
-)
-
-data class UsersResponse(
-    val users: List<UserInfo>
-)
-
-/** Response (list's element) type of "GET /bank-accounts" */
-data class BankAccount(
-    var ownerName: String,
-    var iban: String,
-    var bic: String,
-    var nexusBankAccountId: String
-)
-
-data class OfferedBankAccount(
-    var ownerName: String,
-    var iban: String,
-    var bic: String,
-    var offeredAccountId: String,
-    var nexusBankAccountId: String?
-)
-
-data class OfferedBankAccounts(
-    val accounts: MutableList<OfferedBankAccount> = mutableListOf()
-)
-
-/** Response type of "GET /bank-accounts" */
-data class BankAccounts(
-    var accounts: MutableList<BankAccount> = mutableListOf()
-)
-
-data class BankMessageList(
-    val bankMessages: MutableList<BankMessageInfo> = mutableListOf()
-)
-
-data class BankMessageInfo(
-    // x-libeufin-bank messages do not have any ID or code.
-    val messageId: String?,
-    val code: String?,
-    val length: Long
-)
-
-data class FacadeShowInfo(
-    val name: String,
-    val type: String,
-    // Taler wire gateway API base URL.
-    // Different from the base URL of the facade.
-    val baseUrl: String,
-    val config: JsonNode
-)
-
-data class FacadeInfo(
-    val name: String,
-    val type: String,
-    val bankAccountsRead: MutableList<String>? = mutableListOf(),
-    val bankAccountsWrite: MutableList<String>? = mutableListOf(),
-    val bankConnectionsRead: MutableList<String>? = mutableListOf(),
-    val bankConnectionsWrite: MutableList<String>? = mutableListOf(),
-    val config: TalerWireGatewayFacadeConfig /* To be abstracted to Any! */
-)
-
-data class TalerWireGatewayFacadeConfig(
-    val bankAccount: String,
-    val bankConnection: String,
-    val reserveTransferLevel: String,
-    val currency: String
-)
-
-data class Pain001Data(
-    val creditorIban: String,
-    val creditorBic: String?,
-    val creditorName: String,
-    val sum: String,
-    val currency: String,
-    val subject: String,
-    val endToEndId: String? = null
-)
-
-data class AccountTask(
-    val resourceType: String,
-    val resourceId: String,
-    val taskName: String,
-    val taskType: String,
-    val taskCronspec: String,
-    val taskParams: String,
-    val nextScheduledExecutionSec: Long?, // human-readable time (= Epoch when 
this value doesn't exist in DB)
-    val prevScheduledExecutionSec: Long? // human-readable time (= Epoch when 
this value doesn't exist in DB)
-)
-
-data class CreateAccountTaskRequest(
-    val name: String,
-    val cronspec: String,
-    val type: String,
-    val params: JsonNode
-)
-
-data class ImportBankAccount(
-    val offeredAccountId: String,
-    val nexusBankAccountId: String
-)
-
-data class InitiatedPayments(
-    val initiatedPayments: MutableList<PaymentStatus> = mutableListOf()
-)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
deleted file mode 100644
index ef5ff781..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ /dev/null
@@ -1,1182 +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.nexus.server
-
-import UtilError
-import io.ktor.serialization.jackson.*
-import io.ktor.server.plugins.contentnegotiation.*
-import com.fasterxml.jackson.core.util.DefaultIndenter
-import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.DeserializationFeature
-import com.fasterxml.jackson.databind.JsonMappingException
-import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.module.kotlin.*
-import io.ktor.client.*
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.plugins.*
-import io.ktor.server.plugins.callloging.*
-import io.ktor.server.plugins.statuspages.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.slf4j.event.Level
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.bankaccount.*
-import tech.libeufin.nexus.ebics.*
-import tech.libeufin.nexus.iso20022.ingestCamtMessageIntoAccount
-import tech.libeufin.util.*
-import java.net.URLEncoder
-import tech.libeufin.nexus.logger
-
-// Return facade state depending on the type.
-fun getFacadeState(type: String, facade: FacadeEntity): JsonNode {
-    return transaction {
-        when (type) {
-            "taler-wire-gateway",
-            "anastasis" -> {
-                val state = FacadeStateEntity.find {
-                    FacadeStateTable.facade eq facade.id
-                }.firstOrNull()
-                if (state == null) throw NexusError(
-                    HttpStatusCode.NotFound,
-                    "State of facade ${facade.id} not found"
-                )
-                val node = jacksonObjectMapper().createObjectNode()
-                node.put("bankConnection", state.bankConnection)
-                node.put("bankAccount", state.bankAccount)
-                node
-            }
-            else -> throw NexusError(
-                HttpStatusCode.NotFound,
-                "Facade type $type not supported"
-            )
-        }
-    }
-}
-
-
-fun ensureNonNull(param: String?): String {
-    return param ?: throw NexusError(
-        HttpStatusCode.BadRequest, "Bad ID given: $param"
-    )
-}
-
-fun ensureLong(param: String?): Long {
-    val asString = ensureNonNull(param)
-    return asString.toLongOrNull() ?: throw NexusError(
-        HttpStatusCode.BadRequest, "Parameter is not Long: $param"
-    )
-}
-
-fun <T> expectNonNull(param: T?): T {
-    return param ?: throw NexusError(
-        HttpStatusCode.BadRequest,
-        "Non-null value expected."
-    )
-}
-
-
-fun ApplicationRequest.hasBody(): Boolean {
-    if (this.isChunked()) {
-        return true
-    }
-    val contentLengthHeaderStr = this.headers["content-length"]
-    if (contentLengthHeaderStr != null) {
-        return try {
-            val cl = contentLengthHeaderStr.toInt()
-            cl != 0
-        } catch (e: NumberFormatException) {
-            false
-        }
-    }
-    return false
-}
-
-fun ApplicationCall.expectUrlParameter(name: String): String {
-    return this.request.queryParameters[name]
-        ?: throw NexusError(HttpStatusCode.BadRequest, "Parameter '$name' not 
provided in URI")
-}
-
-fun requireBankConnectionInternal(connId: String): NexusBankConnectionEntity {
-    return transaction {
-        NexusBankConnectionEntity.find { 
NexusBankConnectionsTable.connectionId eq connId }.firstOrNull()
-    }
-        ?: throw NexusError(HttpStatusCode.NotFound, "bank connection 
'$connId' not found")
-}
-
-fun requireBankConnection(call: ApplicationCall, parameterKey: String): 
NexusBankConnectionEntity {
-    val name = call.parameters[parameterKey]
-    if (name == null) {
-        throw NexusError(
-            HttpStatusCode.NotFound,
-            "Parameter '${parameterKey}' wasn't found in URI"
-        )
-    }
-    return requireBankConnectionInternal(name)
-}
-
-val client = HttpClient { followRedirects = true }
-val nexusApp: Application.() -> Unit = {
-    install(CallLogging) {
-        this.level = Level.DEBUG
-        this.logger = tech.libeufin.nexus.logger
-    }
-    install(LibeufinDecompressionPlugin)
-    install(ContentNegotiation) {
-        jackson {
-            enable(SerializationFeature.INDENT_OUTPUT)
-            setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
-                
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
-                indentObjectsWith(DefaultIndenter("  ", "\n"))
-            })
-            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()
-            )
-            configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
-        }
-    }
-    install(StatusPages) {
-        exception<NexusError> { call, cause ->
-            logger.error("Caught exception while handling '${call.request.uri} 
(${cause.message})")
-            call.respond(
-                status = cause.statusCode,
-                message = ErrorResponse(
-                    code = 
TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_GENERIC_ERROR.code,
-                    hint = "nexus error, see detail",
-                    detail = cause.reason,
-                )
-            )
-        }
-        exception<JsonMappingException> { call, cause ->
-            logger.error("Exception while handling '${call.request.uri}'", 
cause.message)
-            call.respond(
-                HttpStatusCode.BadRequest,
-                message = ErrorResponse(
-                    code = TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID.code,
-                    hint = "POSTed data was not valid",
-                    detail = cause.message ?: "not given",
-                )
-            )
-        }
-        exception<UtilError> { call, cause ->
-            logger.error("Exception while handling '${call.request.uri}': 
${cause.message}")
-            call.respond(
-                cause.statusCode,
-                message = ErrorResponse(
-                    code = cause.ec?.code ?: TalerErrorCode.TALER_EC_NONE.code,
-                    hint = "see detail",
-                    detail = cause.reason,
-                )
-            )
-        }
-        exception<EbicsProtocolError> { call, cause ->
-            logger.error("Caught exception while handling 
'${call.request.uri}' (${cause.message})")
-            call.respond(
-                cause.httpStatusCode,
-                message = ErrorResponse(
-                    code = 
TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_GENERIC_ERROR.code,
-                    hint = "The EBICS communication with the bank failed: 
${cause.ebicsTechnicalCode}",
-                    detail = cause.reason,
-                )
-            )
-        }
-        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 NexusError(
-                    HttpStatusCode.InternalServerError,
-                    "Did not find bad request details."
-                )
-            }
-            logger.error(errorMessage)
-            call.respond(
-                HttpStatusCode.BadRequest,
-                ErrorResponse(
-                    code = 
TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_GENERIC_ERROR.code,
-                    detail = errorMessage,
-                    hint = "Malformed request or unacceptable values"
-                )
-            )
-        }
-        exception<Exception> { call, cause ->
-            logger.error(
-                "Uncaught exception while handling '${call.request.uri}'",
-                cause.stackTraceToString()
-            )
-            cause.printStackTrace()
-            call.respond(
-                HttpStatusCode.InternalServerError,
-                ErrorResponse(
-                    code = 
TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION.code,
-                    hint = "unexpected exception",
-                    detail = "exception message: ${cause.message}",
-                )
-            )
-        }
-    }
-    intercept(ApplicationCallPipeline.Fallback) {
-        if (this.call.response.status() == null) {
-            call.respondText("Not found (no route matched).\n", 
ContentType.Text.Plain, HttpStatusCode.NotFound)
-            return@intercept finish()
-        }
-    }
-    routing {
-        get("/config") {
-            call.respond(
-                makeJsonObject {
-                    prop("version", "0:0:0")
-                    prop("name", "nexus-native")
-                }
-            )
-            return@get
-        }
-        // Shows information about the requesting user.
-        get("/user") {
-            val ret = transaction {
-                val currentUser = authenticateRequest(call.request)
-                UserResponse(
-                    username = currentUser.username,
-                    superuser = currentUser.superuser
-                )
-            }
-            call.respond(ret)
-            return@get
-        }
-
-        get("/permissions") {
-            val resp = object {
-                val permissions = mutableListOf<Permission>()
-            }
-            transaction {
-                requireSuperuser(call.request)
-                NexusPermissionEntity.all().map {
-                    resp.permissions.add(
-                        Permission(
-                            subjectType = it.subjectType,
-                            subjectId = it.subjectId,
-                            resourceType = it.resourceType,
-                            resourceId = it.resourceId,
-                            permissionName = it.permissionName,
-                        )
-                    )
-                }
-            }
-            call.respond(resp)
-        }
-
-        post("/permissions") {
-            val req = call.receive<ChangePermissionsRequest>()
-            val knownPermissions = listOf(
-                "facade.talerwiregateway.history", 
"facade.talerwiregateway.transfer",
-                "facade.anastasis.history"
-            )
-            val permName = req.permission.permissionName.lowercase()
-            if (!knownPermissions.contains(permName)) {
-                throw NexusError(
-                    HttpStatusCode.BadRequest,
-                    "Permission $permName not known"
-                )
-            }
-            transaction {
-                requireSuperuser(call.request)
-                val existingPerm = findPermission(req.permission)
-                when (req.action) {
-                    PermissionChangeAction.GRANT -> {
-                        if (existingPerm == null) {
-                            NexusPermissionEntity.new {
-                                subjectType = req.permission.subjectType
-                                subjectId = req.permission.subjectId
-                                resourceType = req.permission.resourceType
-                                resourceId = req.permission.resourceId
-                                permissionName = permName
-
-                            }
-                        }
-                    }
-                    PermissionChangeAction.REVOKE -> {
-                        existingPerm?.delete()
-                    }
-                }
-                null
-            }
-            call.respond(object {})
-        }
-
-        get("/users") {
-            transaction {
-                requireSuperuser(call.request)
-            }
-            val users = transaction {
-                transaction {
-                    NexusUserEntity.all().map {
-                        UserInfo(it.username, it.superuser)
-                    }
-                }
-            }
-            val usersResp = UsersResponse(users)
-            call.respond(usersResp)
-            return@get
-        }
-
-        // change a user's password
-        post("/users/{username}/password") {
-            val body = call.receive<ChangeUserPassword>()
-            val targetUsername = ensureNonNull(call.parameters["username"])
-            transaction {
-                requireSuperuser(call.request)
-                val targetUser = NexusUserEntity.find {
-                    NexusUsersTable.username eq targetUsername
-                }.firstOrNull()
-                if (targetUser == null) throw NexusError(
-                    HttpStatusCode.NotFound,
-                    "Username $targetUsername not found"
-                )
-                targetUser.passwordHash = CryptoUtil.hashpw(body.newPassword)
-            }
-            call.respond(NexusMessage(message = "Password successfully 
changed"))
-            return@post
-        }
-
-        // Add a new ordinary user in the system (requires superuser 
privileges)
-        post("/users") {
-            requireSuperuser(call.request)
-            val body = call.receive<CreateUserRequest>()
-            val requestedUsername = requireValidResourceName(body.username)
-            transaction {
-                // check if username is available
-                val checkUsername = NexusUserEntity.find {
-                    NexusUsersTable.username eq requestedUsername
-                }.firstOrNull()
-                if (checkUsername != null) throw NexusError(
-                    HttpStatusCode.Conflict,
-                    "Username $requestedUsername unavailable"
-                )
-                NexusUserEntity.new {
-                    username = requestedUsername
-                    passwordHash = CryptoUtil.hashpw(body.password)
-                    superuser = false
-                }
-            }
-            call.respond(
-                NexusMessage(
-                    message = "New user '${body.username}' registered"
-                )
-            )
-            return@post
-        }
-
-        get("/bank-connection-protocols") {
-            requireSuperuser(call.request)
-            call.respond(
-                HttpStatusCode.OK,
-                BankProtocolsResponse(listOf("ebics"))
-            )
-            return@get
-        }
-
-        route("/bank-connection-protocols/ebics") {
-            ebicsBankProtocolRoutes(client)
-        }
-
-        // Shows the bank accounts belonging to the requesting user.
-        get("/bank-accounts") {
-            requireSuperuser(call.request)
-            val bankAccounts = BankAccounts()
-            transaction {
-                NexusBankAccountEntity.all().forEach {
-                    bankAccounts.accounts.add(
-                        BankAccount(
-                            ownerName = it.accountHolder,
-                            iban = it.iban,
-                            bic = it.bankCode,
-                            nexusBankAccountId = it.bankAccountName
-                        )
-                    )
-                }
-            }
-            call.respond(bankAccounts)
-            return@get
-        }
-        post("/bank-accounts/{accountId}/test-camt-ingestion/{type}") {
-            requireSuperuser(call.request)
-            val accountId = ensureNonNull(call.parameters["accountId"])
-            val bankAccount = getBankAccount(accountId)
-            val connId = transaction { 
bankAccount.defaultBankConnection?.connectionId }
-            val dialect = if (connId != null) {
-                val defaultConn = getBankConnection(connId)
-                defaultConn.dialect
-            } else null
-            val msgType = ensureNonNull(call.parameters["type"])
-            ingestCamtMessageIntoAccount(
-                ensureNonNull(accountId),
-                XMLUtil.parseStringIntoDom(call.receiveText()),
-                when(msgType) {
-                    "C52", "Z52" -> { FetchLevel.REPORT }
-                    "C53", "Z53" -> { FetchLevel.STATEMENT }
-                    "C54", "Z54" -> { FetchLevel.NOTIFICATION }
-                    else -> throw badRequest("Message type: '$msgType', not 
supported")
-                },
-                dialect = dialect
-            )
-            call.respond(object {})
-            return@post
-        }
-        get("/bank-accounts/{accountId}/schedule") {
-            requireSuperuser(call.request)
-            val resp = jacksonObjectMapper().createObjectNode()
-            val ops = jacksonObjectMapper().createObjectNode()
-            val accountId = ensureNonNull(call.parameters["accountId"])
-            resp.set<JsonNode>("schedule", ops)
-            transaction {
-                NexusBankAccountEntity.findByName(accountId)
-                    ?: throw unknownBankAccount(accountId)
-                NexusScheduledTaskEntity.find {
-                    (NexusScheduledTasksTable.resourceType eq "bank-account") 
and
-                            (NexusScheduledTasksTable.resourceId eq accountId)
-
-                }.forEach {
-                    val t = jacksonObjectMapper().createObjectNode()
-                    ops.set<JsonNode>(it.taskName, t)
-                    t.put("cronspec", it.taskCronspec)
-                    t.put("type", it.taskType)
-                    t.set<JsonNode>("params", 
jacksonObjectMapper().readTree(it.taskParams))
-                }
-            }
-            call.respond(resp)
-            return@get
-        }
-
-        post("/bank-accounts/{accountId}/schedule") {
-            requireSuperuser(call.request)
-            val schedSpec = call.receive<CreateAccountTaskRequest>()
-            val accountId = ensureNonNull(call.parameters["accountId"])
-            transaction {
-                NexusBankAccountEntity.findByName(accountId)
-                    ?: throw unknownBankAccount(accountId)
-                try {
-                    NexusCron.parser.parse(schedSpec.cronspec)
-                } catch (e: IllegalArgumentException) {
-                    throw NexusError(HttpStatusCode.BadRequest, "bad cron 
spec: ${e.message}")
-                }
-                // sanity checks.
-                when (schedSpec.type) {
-                    "fetch" -> {
-                        jacksonObjectMapper().treeToValue(schedSpec.params, 
FetchSpecJson::class.java)
-                            ?: throw NexusError(HttpStatusCode.BadRequest, 
"bad fetch spec")
-                    }
-                    "submit" -> {
-                    }
-                    else -> throw NexusError(HttpStatusCode.BadRequest, 
"unsupported task type")
-                }
-                val oldSchedTask = NexusScheduledTaskEntity.find {
-                    (NexusScheduledTasksTable.taskName eq schedSpec.name) and
-                            (NexusScheduledTasksTable.resourceType eq 
"bank-account") and
-                            (NexusScheduledTasksTable.resourceId eq accountId)
-
-                }.firstOrNull()
-                if (oldSchedTask != null) {
-                    throw NexusError(HttpStatusCode.BadRequest, "schedule task 
already exists")
-                }
-                NexusScheduledTaskEntity.new {
-                    resourceType = "bank-account"
-                    resourceId = accountId
-                    this.taskCronspec = schedSpec.cronspec
-                    this.taskName = requireValidResourceName(schedSpec.name)
-                    this.taskType = schedSpec.type
-                    this.taskParams =
-                        
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(schedSpec.params)
-                }
-            }
-            call.respond(object {})
-            return@post
-        }
-
-        get("/bank-accounts/{accountId}/schedule/{taskId}") {
-            requireSuperuser(call.request)
-            val taskId = ensureNonNull(call.parameters["taskId"])
-            val task = transaction {
-                NexusScheduledTaskEntity.find {
-                    NexusScheduledTasksTable.taskName eq taskId
-                }.firstOrNull()
-            }
-            if (task == null) throw NexusError(HttpStatusCode.NotFound, "Task 
${taskId} wasn't found")
-            call.respond(
-                AccountTask(
-                    resourceId = task.resourceId,
-                    resourceType = task.resourceType,
-                    taskName = task.taskName,
-                    taskCronspec = task.taskCronspec,
-                    taskType = task.taskType,
-                    taskParams = task.taskParams,
-                    nextScheduledExecutionSec = task.nextScheduledExecutionSec,
-                    prevScheduledExecutionSec = task.prevScheduledExecutionSec
-                )
-            )
-            return@get
-        }
-
-        delete("/bank-accounts/{accountId}/schedule/{taskId}") {
-            requireSuperuser(call.request)
-            logger.info("schedule delete requested")
-            val accountId = ensureNonNull(call.parameters["accountId"])
-            val taskId = ensureNonNull(call.parameters["taskId"])
-            transaction {
-                val bankAccount = NexusBankAccountEntity.findByName(accountId)
-                if (bankAccount == null) {
-                    throw unknownBankAccount(accountId)
-                }
-                val oldSchedTask = NexusScheduledTaskEntity.find {
-                    (NexusScheduledTasksTable.taskName eq taskId) and
-                            (NexusScheduledTasksTable.resourceType eq 
"bank-account") and
-                            (NexusScheduledTasksTable.resourceId eq accountId)
-
-                }.firstOrNull()
-                if (oldSchedTask == null)
-                    throw notFound("Task $taskId is not found.")
-                oldSchedTask.delete()
-            }
-            call.respond(object {})
-        }
-
-        get("/bank-accounts/{accountid}") {
-            requireSuperuser(call.request)
-            val accountId = ensureNonNull(call.parameters["accountid"])
-            val res = transaction {
-                val bankAccount = NexusBankAccountEntity.findByName(accountId)
-                if (bankAccount == null) {
-                    throw unknownBankAccount(accountId)
-                }
-                val holderEnc = URLEncoder.encode(bankAccount.accountHolder, 
Charsets.UTF_8)
-                val lastSeenBalance = NexusBankBalanceEntity.find {
-                    NexusBankBalancesTable.bankAccount eq bankAccount.id
-                }.lastOrNull()
-                return@transaction makeJsonObject {
-                    prop("defaultBankConnection", 
bankAccount.defaultBankConnection?.id?.value)
-                    prop("accountPaytoUri", 
"payto://iban/${bankAccount.iban}?receiver-name=$holderEnc")
-                    prop(
-                        "lastSeenBalance",
-                        if (lastSeenBalance != null) {
-                            val sign = if 
(lastSeenBalance.creditDebitIndicator == "DBIT") "-" else ""
-                            "${sign}${lastSeenBalance.balance}"
-                        } else {
-                            "not downloaded from the bank yet"
-                        }
-                    )
-                }
-            }
-            call.respond(res)
-        }
-
-        // Submit one particular payment to the bank.
-        post("/bank-accounts/{accountid}/payment-initiations/{uuid}/submit") {
-            requireSuperuser(call.request)
-            val uuid = ensureLong(call.parameters["uuid"])
-            submitPaymentInitiation(client, uuid)
-            call.respondText("Payment $uuid submitted")
-            return@post
-        }
-
-        post("/bank-accounts/{accountid}/submit-all-payment-initiations") {
-            requireSuperuser(call.request)
-            val accountId = ensureNonNull(call.parameters["accountid"])
-            submitAllPaymentInitiations(client, accountId)
-            call.respond(object {})
-            return@post
-        }
-
-        get("/bank-accounts/{accountid}/payment-initiations") {
-            requireSuperuser(call.request)
-            val ret = InitiatedPayments()
-            transaction {
-                val bankAccount = requireBankAccount(call, "accountid")
-                PaymentInitiationEntity.find {
-                    PaymentInitiationsTable.bankAccount eq bankAccount.id.value
-                }.forEach {
-                    val sd = it.submissionDate
-                    ret.initiatedPayments.add(
-                        PaymentStatus(
-                            status = it.confirmationTransaction?.status,
-                            paymentInitiationId = it.id.value.toString(),
-                            submitted = it.submitted,
-                            creditorIban = it.creditorIban,
-                            creditorName = it.creditorName,
-                            creditorBic = it.creditorBic,
-                            amount = "${it.currency}:${it.sum}",
-                            subject = it.subject,
-                            submissionDate = if (sd != null) {
-                                importDateFromMillis(sd).toDashedDate()
-                            } else null,
-                            preparationDate = 
importDateFromMillis(it.preparationDate).toDashedDate()
-                        )
-                    )
-                }
-            }
-            call.respond(ret)
-            return@get
-        }
-
-        // Shows information about one particular payment initiation.
-        get("/bank-accounts/{accountid}/payment-initiations/{uuid}") {
-            requireSuperuser(call.request)
-            val res = transaction {
-                val paymentInitiation = 
getPaymentInitiation(ensureLong(call.parameters["uuid"]))
-                return@transaction object {
-                    val paymentInitiation = paymentInitiation
-                    val paymentStatus = 
paymentInitiation.confirmationTransaction?.status
-                }
-            }
-            val sd = res.paymentInitiation.submissionDate
-            call.respond(
-                PaymentStatus(
-                    paymentInitiationId = 
res.paymentInitiation.id.value.toString(),
-                    submitted = res.paymentInitiation.submitted,
-                    creditorName = res.paymentInitiation.creditorName,
-                    creditorBic = res.paymentInitiation.creditorBic,
-                    creditorIban = res.paymentInitiation.creditorIban,
-                    amount = 
"${res.paymentInitiation.currency}:${res.paymentInitiation.sum}",
-                    subject = res.paymentInitiation.subject,
-                    submissionDate = if (sd != null) {
-                        importDateFromMillis(sd).toDashedDate()
-                    } else null,
-                    status = res.paymentStatus,
-                    preparationDate = 
importDateFromMillis(res.paymentInitiation.preparationDate).toDashedDate()
-                )
-            )
-            return@get
-        }
-
-        delete("/bank-accounts/{accountId}/payment-initiations/{uuid}") {
-            requireSuperuser(call.request)
-            val uuid = ensureLong(call.parameters["uuid"])
-            transaction {
-                val paymentInitiation = getPaymentInitiation(uuid)
-                paymentInitiation.delete()
-            }
-            call.respond(NexusMessage(message = "Payment initiation $uuid 
deleted"))
-        }
-
-        // Adds a new payment initiation.
-        post("/bank-accounts/{accountid}/payment-initiations") {
-            requireSuperuser(call.request)
-            val body = call.receive<CreatePaymentInitiationRequest>()
-            val accountId = ensureNonNull(call.parameters["accountid"])
-            if (!validateBic(body.bic)) {
-                throw NexusError(HttpStatusCode.BadRequest, "invalid BIC 
(${body.bic})")
-            }
-            // Handle first idempotence.
-            if (body.uid != null) {
-                val maybeExists: PaymentInitiationEntity? = transaction {
-                    PaymentInitiationEntity.find {
-                        PaymentInitiationsTable.endToEndId eq body.uid
-                    }.firstOrNull()
-                }
-                // If submitted payment looks exactly the same as the one
-                // found in the database, then respond 200 OK.  Otherwise,
-                // it's 409 Conflict.
-                if (maybeExists != null &&
-                    maybeExists.creditorIban == body.iban &&
-                    maybeExists.creditorName == body.name &&
-                    maybeExists.subject == body.subject &&
-                    maybeExists.creditorBic == body.bic &&
-                    "${maybeExists.currency}:${maybeExists.sum}" == body.amount
-                ) {
-                    call.respond(
-                        HttpStatusCode.OK,
-                        PaymentInitiationResponse(uuid = 
maybeExists.id.value.toString())
-                    )
-                    return@post
-                }
-                // The payment was found, but it didn't fulfill the previous 
check,
-                // conflict.
-                if (maybeExists != null)
-                    throw conflict(
-                        "Payment initiation with UID '${body.uid}' " +
-                                "was found already, with different details."
-                    )
-                // If the flow reaches here, then the payment wasn't found
-                // => proceed to create one.
-            }
-            val res = transaction {
-                val bankAccount = getBankAccount(accountId)
-                val amount = parseAmount(body.amount)
-                val paymentEntity = addPaymentInitiation(
-                    Pain001Data(
-                        creditorIban = body.iban,
-                        creditorBic = body.bic,
-                        creditorName = body.name,
-                        sum = amount.amount,
-                        currency = amount.currency,
-                        subject = body.subject,
-                        endToEndId = body.uid
-                    ),
-                    bankAccount
-                )
-                return@transaction object {
-                    val uuid = paymentEntity.id.value
-                }
-            }
-            call.respond(
-                HttpStatusCode.OK,
-                PaymentInitiationResponse(uuid = res.uuid.toString())
-            )
-            return@post
-        }
-
-        // Downloads new transactions from the bank.
-        post("/bank-accounts/{accountid}/fetch-transactions") {
-            requireSuperuser(call.request)
-            val accountid = call.parameters["accountid"]
-            if (accountid == null) {
-                throw NexusError(
-                    HttpStatusCode.BadRequest,
-                    "Account id missing"
-                )
-            }
-            val fetchSpec = if (call.request.hasBody()) {
-                call.receive<FetchSpecJson>()
-            } else {
-                logger.warn("fetch-transactions wants statements (they aren't 
implemented at the bank)")
-                FetchSpecLatestJson(
-                    FetchLevel.STATEMENT,
-                    null
-                )
-            }
-            val ingestionResult = fetchBankAccountTransactions(client, 
fetchSpec, accountid)
-            var statusCode = HttpStatusCode.OK
-            /**
-             * Client errors are unlikely here, because authentication
-             * and JSON validity fail earlier.  Hence, either Nexus or the
-             * bank had a problem.  NOTE: because this handler triggers 
multiple
-             * fetches, it is ALSO possible that although one error is 
reported,
-             * SOME transactions made it to the database!
-             */
-            if (ingestionResult.errors != null) {
-                /**
-                 * Nexus could not handle the error (regardless of it being 
generated
-                 * here or gotten from the bank).  The response body should 
inform the
-                 * client about what failed.
-                 */
-                statusCode = HttpStatusCode.InternalServerError
-            }
-
-            call.respond(
-                status = statusCode,
-                object {
-                    val newTransactions = ingestionResult.newTransactions
-                    val downloadedTransactions = 
ingestionResult.downloadedTransactions
-                    val errors = mutableListOf<String>().apply {
-                        ingestionResult.errors?.forEach {
-                            this.add(it.message ?: "Error message not found.")
-                        }
-                    }
-                }
-            )
-            return@post
-        }
-
-        // Asks list of transactions ALREADY downloaded from the bank.
-        get("/bank-accounts/{accountid}/transactions") {
-            requireSuperuser(call.request)
-            val accountLabel = expectNonNull(call.parameters["accountid"])
-            // Getting the URI parameters.
-            val maybeStart = call.maybeLong("start") // Earliest TX in the 
result.
-            val maybeSize = call.maybeLong("size") // How many TXs at most.
-            val maybeLongPoll = call.maybeLong("long_poll_ms")
-
-            // Ask for a DB event (before the actual query),
-            // in case the DB is Postgres and the client wants.
-            val listenHandle = if (isPostgres() && maybeLongPoll != null) {
-                val channelName = buildChannelName(
-                    NotificationsChannelDomains.LIBEUFIN_NEXUS_TX,
-                    accountLabel
-                )
-                val listenHandle = PostgresListenHandle(channelName)
-                listenHandle.postgresListen()
-                listenHandle
-            } else null
-
-            // Try getting results, and UNLISTEN in case they exist.
-            val queryParam = GetTransactionsParams(
-                bankAccountId = accountLabel,
-                resultSize = maybeSize ?: 5,
-                startIndex = maybeStart ?: 1
-            )
-            var ret = getIngestedTransactions(queryParam)
-            if (ret.isNotEmpty() && listenHandle != null)
-                listenHandle.postgresUnlisten() // closes the PG connection 
too.
-
-            // No results and a DB event is pending: wait.
-            if (ret.isEmpty() && listenHandle != null && maybeLongPoll != 
null) {
-                val isNotificationArrived = 
listenHandle.waitOnIODispatchers(maybeLongPoll)
-                // The event happened, query again.
-                if (isNotificationArrived)
-                    ret = getIngestedTransactions(queryParam)
-            }
-            call.respond(object {val transactions = ret})
-            return@get
-        }
-
-        // Adds a new bank transport.
-        post("/bank-connections") {
-            requireSuperuser(call.request)
-            // user exists and is authenticated.
-            val body = call.receive<CreateBankConnectionRequestJson>()
-            requireValidResourceName(body.name)
-            transaction {
-                val user = authenticateRequest(call.request)
-                val existingConn =
-                    NexusBankConnectionEntity.find { 
NexusBankConnectionsTable.connectionId eq body.name }
-                        .firstOrNull()
-                if (existingConn != null) {
-                    // FIXME: make idempotent.
-                    throw NexusError(HttpStatusCode.Conflict, "connection 
'${body.name}' exists already")
-                }
-                when (body) {
-                    is CreateBankConnectionFromBackupRequestJson -> {
-                        val type = body.data.get("type")
-                        if (type == null || !type.isTextual) {
-                            throw NexusError(
-                                HttpStatusCode.BadRequest,
-                                "backup needs type"
-                            )
-                        }
-                        val plugin = getConnectionPlugin(type.textValue())
-                        plugin.createConnectionFromBackup(
-                            body.name,
-                            user,
-                            body.passphrase,
-                            body.data
-                        )
-                    }
-                    is CreateBankConnectionFromNewRequestJson -> {
-                        val plugin = getConnectionPlugin(body.type)
-                        plugin.createConnection(
-                            body.name,
-                            user,
-                            body.data
-                        )
-                    }
-                }
-            }
-            call.respond(object {})
-        }
-
-        post("/bank-connections/delete-connection") {
-            requireSuperuser(call.request)
-            val body = call.receive<BankConnectionDeletion>()
-            transaction {
-                val conn =
-                    NexusBankConnectionEntity.find { 
NexusBankConnectionsTable.connectionId eq body.bankConnectionId }
-                        .firstOrNull() ?: throw NexusError(
-                        HttpStatusCode.NotFound,
-                        "Bank connection ${body.bankConnectionId}"
-                    )
-                conn.delete() // temporary, and instead just _mark_ it as 
deleted?
-            }
-            call.respond(object {})
-        }
-
-        get("/bank-connections") {
-            requireSuperuser(call.request)
-            val connList = BankConnectionsList()
-            transaction {
-                NexusBankConnectionEntity.all().forEach {
-                    connList.bankConnections.add(
-                        BankConnectionInfo(
-                            name = it.connectionId,
-                            type = it.type
-                        )
-                    )
-                }
-            }
-            call.respond(connList)
-        }
-
-        get("/bank-connections/{connectionName}") {
-            requireSuperuser(call.request)
-            val resp = transaction {
-                val conn = requireBankConnection(call, "connectionName")
-                getConnectionPlugin(conn.type).getConnectionDetails(conn)
-            }
-            call.respond(resp)
-        }
-
-        post("/bank-connections/{connectionName}/export-backup") {
-            requireSuperuser(call.request)
-            val body = call.receive<BackupRequestJson>()
-            val response = run {
-                val conn = requireBankConnection(call, "connectionName")
-                getConnectionPlugin(conn.type).exportBackup(conn.connectionId, 
body.passphrase)
-            }
-            call.response.headers.append("Content-Disposition", "attachment")
-            call.respond(
-                HttpStatusCode.OK,
-                response
-            )
-        }
-
-        post("/bank-connections/{connectionName}/connect") {
-            requireSuperuser(call.request)
-            val conn = transaction {
-                requireBankConnection(call, "connectionName")
-            }
-            val plugin = getConnectionPlugin(conn.type)
-            plugin.connect(client, conn.connectionId)
-            call.respond(NexusMessage(message = "Connection successful"))
-        }
-
-        get("/bank-connections/{connectionName}/keyletter") {
-            requireSuperuser(call.request)
-            val conn = transaction {
-                requireBankConnection(call, "connectionName")
-            }
-            val pdfBytes = 
getConnectionPlugin(conn.type).exportAnalogDetails(conn)
-            call.respondBytes(pdfBytes, ContentType("application", "pdf"))
-        }
-
-        get("/bank-connections/{connectionName}/messages") {
-            requireSuperuser(call.request)
-            val ret = transaction {
-                val list = BankMessageList()
-                val conn = requireBankConnection(call, "connectionName")
-                NexusBankMessageEntity.find { 
NexusBankMessagesTable.bankConnection eq conn.id }.map {
-                    list.bankMessages.add(
-                        BankMessageInfo(
-                            messageId = it.messageId,
-                            code = it.fetchLevel.jsonName,
-                            length = it.message.bytes.size.toLong()
-                        )
-                    )
-                }
-                list
-            }
-            call.respond(ret)
-        }
-
-        get("/bank-connections/{connid}/messages/{msgid}") {
-            requireSuperuser(call.request)
-            val ret = transaction {
-                val msgid = call.parameters["msgid"]
-                if (msgid == null || msgid == "") {
-                    throw NexusError(HttpStatusCode.BadRequest, "missing or 
invalid message ID")
-                }
-                val msg = NexusBankMessageEntity.find { 
NexusBankMessagesTable.messageId eq msgid }.firstOrNull()
-                    ?: throw NexusError(HttpStatusCode.NotFound, "bank message 
not found")
-                return@transaction object {
-                    val msgContent = msg.message.bytes
-                }
-            }
-            call.respondBytes(ret.msgContent, ContentType("application", 
"xml"))
-        }
-
-        get("/facades/{fcid}") {
-            requireSuperuser(call.request)
-            val fcid = ensureNonNull(call.parameters["fcid"])
-            val ret = transaction {
-                val f = FacadeEntity.findByName(fcid) ?: throw NexusError(
-                    HttpStatusCode.NotFound, "Facade $fcid does not exist"
-                )
-                // FIXME: this only works for TWG urls.
-                FacadeShowInfo(
-                    name = f.facadeName,
-                    type = f.type,
-                    baseUrl = URLBuilder(call.request.getBaseUrl()).apply {
-                        this.appendPathSegments(listOf("facades", 
f.facadeName, f.type))
-                        encodedPath += "/"
-                    }.buildString(),
-                    config = getFacadeState(f.type, f)
-                )
-            }
-            call.respond(ret)
-            return@get
-        }
-
-        get("/facades") {
-            requireSuperuser(call.request)
-            val ret = object {
-                val facades = mutableListOf<FacadeShowInfo>()
-            }
-            transaction {
-                val user = authenticateRequest(call.request)
-                FacadeEntity.find {
-                    FacadesTable.creator eq user.id
-                }.forEach {
-                    ret.facades.add(
-                        FacadeShowInfo(
-                            name = it.facadeName,
-                            type = it.type,
-                            baseUrl = 
URLBuilder(call.request.getBaseUrl()).apply {
-                                this.appendPathSegments(listOf("facades", 
it.facadeName, it.type))
-                                encodedPath += "/"
-                            }.buildString(),
-                            config = getFacadeState(it.type, it)
-                        )
-                    )
-                }
-            }
-            call.respond(ret)
-            return@get
-        }
-
-        delete("/facades/{fcid}") {
-            requireSuperuser(call.request)
-            val fcid = ensureNonNull(call.parameters["fcid"])
-            transaction {
-                val f = FacadeEntity.findByName(fcid) ?: throw NexusError(
-                    HttpStatusCode.NotFound,
-                    "Facade $fcid does not exist"
-                )
-                f.delete()
-            }
-            call.respond({})
-            return@delete
-        }
-
-        post("/facades") {
-            val user = requireSuperuser(call.request)
-            val body = call.receive<FacadeInfo>()
-            requireValidResourceName(body.name)
-            if (!listOf("taler-wire-gateway", "anastasis").contains(body.type))
-                throw NexusError(
-                    HttpStatusCode.NotImplemented,
-                    "Facade type '${body.type}' is not implemented"
-                )
-            // Check if the facade exists already.
-            val createNewFacade = transaction {
-                val maybeFacade = FacadeEntity.findByName(body.name)
-                // Facade exists, check all the values for idempotence.
-                if (maybeFacade != null) {
-                    // First get the associated config.
-                    val facadeConfig = getFacadeState(maybeFacade.facadeName)
-                    if (maybeFacade.type != body.type
-                        || maybeFacade.creator.username != user.username
-                        || facadeConfig.bankAccount != body.config.bankAccount
-                        || facadeConfig.bankConnection != 
body.config.bankConnection
-                        || facadeConfig.reserveTransferLevel != 
body.config.reserveTransferLevel
-                        || facadeConfig.currency != body.config.currency) {
-                        throw conflict("Facade ${body.name} exists but its 
state differs from the request.")
-                    }
-                    // Facade exists and has exact same values, inhibit 
creation.
-                    else return@transaction false
-                }
-                // Facade does not exist, trigger creation.
-                true
-            }
-            if (createNewFacade) {
-                transaction {
-                    val newFacade = FacadeEntity.new {
-                        facadeName = body.name
-                        type = body.type
-                        creator = user
-                    }
-                    FacadeStateEntity.new {
-                        bankAccount = body.config.bankAccount
-                        bankConnection = body.config.bankConnection
-                        reserveTransferLevel = body.config.reserveTransferLevel
-                        facade = newFacade
-                        currency = body.config.currency
-                    }
-                }
-            }
-            call.respond(HttpStatusCode.OK)
-            return@post
-        }
-
-        route("/bank-connections/{connid}") {
-
-            // only ebics specific tasks under this part.
-            route("/ebics") {
-                ebicsBankConnectionRoutes(client)
-            }
-            post("/fetch-accounts") {
-                requireSuperuser(call.request)
-                val conn = transaction {
-                    requireBankConnection(call, "connid")
-                }
-                getConnectionPlugin(conn.type).fetchAccounts(client, 
conn.connectionId)
-                call.respond(object {})
-            }
-
-            // show all the offered accounts (both imported and non)
-            get("/accounts") {
-                requireSuperuser(call.request)
-                val ret = OfferedBankAccounts()
-                transaction {
-                    val conn = requireBankConnection(call, "connid")
-                    OfferedBankAccountEntity.find {
-                        OfferedBankAccountsTable.bankConnection eq 
conn.id.value
-                    }.forEach { offeredAccount ->
-                        val importedId = offeredAccount.imported?.id
-                        val imported = if (importedId != null) {
-                            NexusBankAccountEntity.findById(importedId)
-                        } else {
-                            null
-                        }
-                        ret.accounts.add(
-                            OfferedBankAccount(
-                                ownerName = offeredAccount.accountHolder,
-                                iban = offeredAccount.iban,
-                                bic = offeredAccount.bankCode,
-                                offeredAccountId = 
offeredAccount.offeredAccountId,
-                                nexusBankAccountId = imported?.bankAccountName
-                            )
-                        )
-                    }
-                }
-                call.respond(ret)
-            }
-
-            // import one account into libeufin.
-            post("/import-account") {
-                requireSuperuser(call.request)
-                val body = call.receive<ImportBankAccount>()
-                importBankAccount(call, body.offeredAccountId, 
body.nexusBankAccountId)
-                call.respond(object {})
-            }
-        }
-        route("/facades/{fcid}/taler-wire-gateway") {
-            talerFacadeRoutes(this)
-        }
-        route("/facades/{fcid}/anastasis") {
-            anastasisFacadeRoutes(this)
-        }
-
-        // Hello endpoint.
-        get("/") {
-            call.respondText("Hello, this is Nexus.\n")
-            return@get
-        }
-    }
-}
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
deleted file mode 100644
index 0eb5f57d..00000000
--- 
a/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2021 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.nexus.server
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.request.*
-import io.ktor.util.*
-import io.ktor.util.pipeline.*
-import io.ktor.utils.io.*
-import io.ktor.utils.io.jvm.javaio.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.util.zip.InflaterInputStream
-
-val LibeufinDecompressionPlugin = 
createApplicationPlugin("RequestingBodyDecompression") {
-    onCallReceive { call ->
-        transformBody { data ->
-            if (call.request.headers[HttpHeaders.ContentEncoding] == 
"deflate") {
-                val brc = withContext(Dispatchers.IO) {
-                    val inflated = InflaterInputStream(data.toInputStream())
-                    @Suppress("BlockingMethodInNonBlockingContext")
-                    val bytes = inflated.readAllBytes()
-                    ByteReadChannel(bytes)
-                }
-                brc
-            } else data
-        }
-    }
-}
\ No newline at end of file
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt
deleted file mode 100644
index 9a433e63..00000000
--- 
a/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt
+++ /dev/null
@@ -1,536 +0,0 @@
-package tech.libeufin.nexus.xlibeufinbank
-
-import AgentIdentification
-import Batch
-import BatchTransaction
-import CamtBankAccountEntry
-import CashAccount
-import CreditDebitIndicator
-import CurrencyAmount
-import PartyIdentification
-import TransactionDetails
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
-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 io.ktor.server.util.*
-import io.ktor.util.*
-import org.jetbrains.exposed.sql.statements.api.ExposedBlob
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.bankaccount.*
-import tech.libeufin.nexus.server.*
-import tech.libeufin.util.*
-import java.net.MalformedURLException
-import java.net.URL
-import java.time.LocalDate
-
-// Gets Sandbox URL and credentials, taking the connection name as input.
-private fun getXLibeufinBankCredentials(conn: NexusBankConnectionEntity): 
XLibeufinBankTransport {
-    val maybeCredentials = transaction {
-        XLibeufinBankUserEntity.find {
-            XLibeufinBankUsersTable.nexusBankConnection eq conn.id
-        }.firstOrNull()
-    }
-    if (maybeCredentials == null) throw internalServerError(
-        "Existing connection ${conn.connectionId} has no transport details"
-    )
-    return XLibeufinBankTransport(
-        username = maybeCredentials.username,
-        password = maybeCredentials.password,
-        baseUrl = maybeCredentials.baseUrl
-    )
-}
-private fun getXLibeufinBankCredentials(connId: String): 
XLibeufinBankTransport {
-    val conn = getBankConnection(connId)
-    return getXLibeufinBankCredentials(conn)
-}
-class XlibeufinBankConnectionProtocol : BankConnectionProtocol {
-    override fun getBankUrl(connId: String): String {
-        return getXLibeufinBankCredentials(connId).baseUrl
-    }
-    /**
-     * Together with checking the credentials, this method downloads
-     * additional details from the bank, and stores them in the table
-     * that holds offered bank accounts.  That saves one call to the
-     * "import" method by recycling the information obtained here.
-     */
-    override suspend fun connect(client: HttpClient, connId: String) {
-        val conn = getBankConnection(connId)
-        val connDetails = getXLibeufinBankCredentials(conn)
-        // Defining the URL to request the bank account balance.
-        val url = connDetails.baseUrl + "/accounts/${connDetails.username}"
-        // Error handling expected by the caller.
-        val details = client.get(url) {
-            expectSuccess = true
-            basicAuth(connDetails.username, connDetails.password)
-        }
-        val txtDetails = details.bodyAsText()
-        val jDetails = jacksonObjectMapper().readTree(txtDetails)
-        val paytoUri: String = try { jDetails.get("paytoUri").asText() }
-        catch (e: Exception) {
-            logger.error("Did not find 'paytoUri' along the connection" +
-                    " operation from x-libeufin-bank connection $connId." +
-                    "  Bank says: $txtDetails"
-            )
-            throw badGateway("Bank missed basic account information 
('paytoUri' field)")
-        }
-        val paytoObj = parsePayto(payto = paytoUri)
-        val maybeOfferedAccount = transaction {
-            OfferedBankAccountEntity.find {
-                // Sandbox reliably names the bank account with the owner's 
username
-                OfferedBankAccountsTable.offeredAccountId eq 
connDetails.username
-            }.firstOrNull()
-        }
-        // Bank account already imported.
-        if (maybeOfferedAccount != null)
-            return
-        // Import it.
-        transaction {
-            OfferedBankAccountEntity.new {
-                offeredAccountId = connDetails.username
-                bankConnection = conn
-                accountHolder = paytoObj.receiverName ?: "Not given by the 
bank"
-                bankCode = paytoObj.bic ?: "SANDBOXX"
-                iban = paytoObj.iban
-            }
-        }
-    }
-
-    /**
-     * This operation is carried along connect(), because as a side
-     * effect of checking the credentials it ALSO gets all the offered
-     * bank account information that this function WOULD have obtained.
-     *
-     * Therefore, this method throws error when called, in order to raise
-     * the clients' awareness not to rely on it.
-     */
-    override suspend fun fetchAccounts(client: HttpClient, connId: String) {
-        throw NotImplementedError("Please skip this method when using 
x-libeufin-bank.")
-    }
-
-    override fun createConnectionFromBackup(
-        connId: String,
-        user: NexusUserEntity,
-        passphrase: String?,
-        backup: JsonNode
-    ) {
-        TODO("Not yet implemented")
-    }
-
-    override fun createConnection(
-        connId: String,
-        user: NexusUserEntity,
-        data: JsonNode
-    ) {
-        val bankConn = transaction {
-            NexusBankConnectionEntity.new {
-                this.connectionId = connId
-                owner = user
-                type = "x-libeufin-bank"
-            }
-        }
-        val newTransportData = jacksonObjectMapper().treeToValue(
-            data, XLibeufinBankTransport::class.java
-        ) ?: throw badRequest("x-libeufin-bank details not found in the 
request")
-        // Validate the base URL
-        try { URL(newTransportData.baseUrl).toURI() }
-        catch (e: MalformedURLException) {
-            throw badRequest("Base URL (${newTransportData.baseUrl}) is 
invalid.")
-        }
-        transaction {
-            XLibeufinBankUserEntity.new {
-                username = newTransportData.username
-                password = newTransportData.password
-                // Only addressing mild cases where ONE slash ends the base 
URL.
-                baseUrl = newTransportData.baseUrl.dropLastWhile { it == '/' }
-                nexusBankConnection = bankConn
-            }
-        }
-    }
-
-    override fun getConnectionDetails(conn: NexusBankConnectionEntity): 
JsonNode {
-        val credentials = getXLibeufinBankCredentials(conn)
-        val mapper = ObjectMapper()
-        val details = mapper.createObjectNode()
-        details.put("baseUrl", credentials.baseUrl)
-        details.put("username", credentials.username)
-        val node = mapper.createObjectNode()
-        node.put("type", conn.type)
-        node.put("owner", conn.owner.username)
-        node.set<JsonNode>("details", details)
-        return node
-    }
-
-    override fun exportBackup(bankConnectionId: String, passphrase: String): 
JsonNode {
-        TODO("Not yet implemented")
-    }
-
-    override fun exportAnalogDetails(conn: NexusBankConnectionEntity): 
ByteArray {
-        throw NotImplementedError("x-libeufin-bank does not need analog 
details")
-    }
-
-    override suspend fun submitPaymentInitiation(
-        httpClient: HttpClient,
-        paymentInitiationId: Long
-    ) {
-        /**
-         * Main steps.
-         *
-         * 1) Get prep from the DB.
-         * 2) Collect credentials.
-         * 3) Create the format to POST.
-         * 4) POST the transaction.
-         * 5) Mark the prep as submitted.
-         * */
-        // 1
-        val preparedPayment = getPaymentInitiation(paymentInitiationId)
-        // 2
-        val conn = transaction { 
preparedPayment.bankAccount.defaultBankConnection } ?: throw
-                internalServerError("Default connection not found for bank 
account: ${preparedPayment.bankAccount.bankAccountName}")
-        val credentials: XLibeufinBankTransport = 
getXLibeufinBankCredentials(conn)
-        // 3
-        val paytoUri = buildIbanPaytoUri(
-            iban = preparedPayment.creditorIban,
-            bic = preparedPayment.creditorBic ?: "SANDBOXX",
-            receiverName = preparedPayment.creditorName,
-            message = preparedPayment.subject
-        )
-        val req = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(
-            XLibeufinBankPaytoReq(
-                paytoUri = paytoUri,
-                amount = "${preparedPayment.currency}:${preparedPayment.sum}",
-                pmtInfId = preparedPayment.paymentInformationId
-            )
-        )
-        // 4
-        val url = credentials.baseUrl + 
"/accounts/${credentials.username}/transactions"
-        logger.debug("POSTing transactions to x-libeufin-bank at: $url")
-        val r = httpClient.post(url) {
-            expectSuccess = false
-            contentType(ContentType.Application.Json)
-            basicAuth(credentials.username, credentials.password)
-            setBody(req)
-        }
-        if (r.status.value.toString().startsWith("5")) {
-            throw NexusError(
-                HttpStatusCode.BadGateway,
-                "The bank failed: ${r.bodyAsText()}"
-            )
-        }
-        if (!r.status.value.toString().startsWith("2")) {
-            throw NexusError(
-                /**
-                 * Echoing whichever status code the bank gave.  That
-                 * however masks client errors where - for example - a
-                 * request detail causes 404 where Nexus has no power.
-                 */
-                HttpStatusCode(r.status.value, r.status.description),
-                r.bodyAsText()
-            )
-        }
-        // 5
-        transaction { preparedPayment.submitted = true }
-    }
-
-    override suspend fun fetchTransactions(
-        fetchSpec: FetchSpecJson, // FIXME: handle time range.
-        client: HttpClient,
-        bankConnectionId: String,
-        accountId: String
-    ): List<Exception>? {
-        val conn = getBankConnection(bankConnectionId)
-        if (fetchSpec.level == FetchLevel.REPORT || fetchSpec.level == 
FetchLevel.ALL)
-            throw badRequest("level '${fetchSpec.level}' on x-libeufin-bank" +
-                    "connection (${conn.connectionId}) is not supported:" +
-                    " bank has only 'booked' state."
-            )
-        // Get credentials
-        val credentials = getXLibeufinBankCredentials(conn)
-        /**
-         * Now builds the URL to ask the transactions, according to the
-         * FetchSpec gotten in the args.  Level 'statement' and time range
-         * 'previous-days' are NOT implemented.
-         */
-        val baseUrl = URL(credentials.baseUrl)
-        val fetchUrl = url {
-            protocol = URLProtocol(name = baseUrl.protocol, defaultPort = -1)
-            port = baseUrl.port
-            appendPathSegments(
-                baseUrl.path.dropLastWhile { it == '/' },
-                "accounts/${credentials.username}/transactions")
-            when (fetchSpec) {
-                is FetchSpecTimeRangeJson -> {
-                    // the parse() method defaults to the YYYY-MM-DD format.
-                    val start: LocalDate = LocalDate.parse(fetchSpec.start)
-                    val end: LocalDate = LocalDate.parse(fetchSpec.end)
-                    this.parameters["from_ms"] = start.millis().toString()
-                    this.parameters["until_ms"] = end.millis().toString()
-                }
-                // Gets the last 5 transactions
-                is FetchSpecLatestJson -> {
-                    // Do nothing, the bare endpoint gets the last 5 txs by 
default.
-                }
-                /* Defines the from_ms URI param. according to the last 
transaction
-                 * timestamp that was seen in this connection */
-                is FetchSpecSinceLastJson -> {
-                    val localBankAccount = getBankAccount(accountId)
-                    // Sandbox doesn't have report vs. statement, defaulting 
to statement time
-                    // and so does the ingestion routine when storing the last 
message time.
-                    // The sought time must be incremented by one because the 
filter is _inclusive_.
-                    this.parameters["from_ms"] = 
"${localBankAccount.lastStatementCreationTimestamp?.plus(1) ?: 0}"
-                }
-                // This wants ALL the transactions, hence it sets the from_ms 
to zero.
-                is FetchSpecAllJson -> {
-                    this.parameters["from_ms"] = "0"
-                }
-                else -> throw NexusError(
-                    HttpStatusCode.NotImplemented,
-                    "FetchSpec ${fetchSpec::class} not supported"
-                )
-            }
-        }
-        logger.debug("Requesting x-libeufin-bank transactions to: $fetchUrl")
-        val resp: HttpResponse = try {
-            client.get(fetchUrl) {
-                expectSuccess = true
-                contentType(ContentType.Application.Json)
-                basicAuth(credentials.username, credentials.password)
-            }
-        } catch (e: Exception) {
-            e.printStackTrace()
-            logger.error(e.message)
-            return listOf(e)
-        }
-        val respBlob = resp.bodyAsChannel().toByteArray()
-        transaction {
-            NexusBankMessageEntity.new {
-                bankConnection = conn
-                message = ExposedBlob(respBlob)
-                fetchLevel = fetchSpec.level
-            }
-        }
-        return null
-    }
-}
-
-fun ingestXLibeufinBankMessage(
-    bankAccountId: String,
-    data: String // JSON
-): IngestedTransactionsCount {
-    val jMessage = try { jacksonObjectMapper().readTree(data) }
-    catch (e: Exception) {
-        logger.error("Bank message $data could not" +
-                " be parsed into JSON by the x-libeufin-bank ingestion.")
-        throw internalServerError("Could not ingest x-libeufin-bank message.")
-    }
-    return ingestXLibeufinBankMessage(bankAccountId, jMessage)
-}
-/**
- * Parses one x-libeufin-bank message and INSERTs Nexus local
- * transaction records into the database.  After this function
- * returns, the transactions are ready to both being communicated
- * to the CLI via the native JSON interface OR being further processed
- * by ANY facade.
- *
- * This function:
- * - updates the local timestamps related to the latest report.
- * - inserts a new NexusBankTransactionEntity.  To achieve that, it extracts 
the:
- * -- amount
- * -- credit/debit indicator
- * -- currency
- *
- * Note: in contrast to what the CaMt handler does, here there's NO
- * status, since Sandbox has only one (unnamed) transaction state and
- * all transactions are asked as reports.
- */
-fun ingestXLibeufinBankMessage(
-    bankAccountId: String,
-    data: JsonNode
-): IngestedTransactionsCount {
-    data class XLibeufinBankTransactions(
-        val transactions: List<XLibeufinBankTransaction>
-    )
-    val txs = try {
-        jacksonObjectMapper().treeToValue(
-            data,
-            XLibeufinBankTransactions::class.java
-        )
-    } catch (e: Exception) {
-        throw NexusError(
-            HttpStatusCode.BadGateway,
-            "The bank sent invalid x-libeufin-bank transactions."
-        )
-    }
-    val bankAccount = getBankAccount(bankAccountId)
-    var newTxs = 0 // Counts how many transactions are new.
-    txs.transactions.forEach {
-        val maybeTimestamp = try {
-            it.date.toLong()
-        } catch (e: Exception) {
-            throw NexusError(
-                HttpStatusCode.BadGateway,
-                "The bank gave an invalid timestamp " +
-                        "for x-libeufin-bank message: ${it.uid}"
-            )
-        }
-        // Searching for duplicates.
-        if (findDuplicate(bankAccountId, 
"${PaymentUidQualifiers.BANK_GIVEN}:${it.uid}") != null) {
-            logger.debug("x-libeufin-bank ingestion: transaction ${it.uid} is 
a duplicate, skipping.")
-            return@forEach
-        }
-        val direction = if (it.debtorIban == bankAccount.iban)
-            XLibeufinBankDirection.DEBIT else XLibeufinBankDirection.CREDIT
-        // New tx, storing it.
-        transaction {
-            val localTx = NexusBankTransactionEntity.new {
-                this.bankAccount = bankAccount
-                this.amount = it.amount
-                this.currency = it.currency
-                /**
-                 * Sandbox has only booked state for its transactions: as soon 
as
-                 * one payment makes it to the database, that is the final 
(booked)
-                 * state.
-                 */
-                this.status = EntryStatus.BOOK
-                this.accountTransactionId = 
"${PaymentUidQualifiers.BANK_GIVEN}:${it.uid}"
-                this.transactionJson = jacksonObjectMapper(
-                ).writeValueAsString(it.exportAsCamtModel())
-                this.creditDebitIndicator = direction.exportAsCamtDirection()
-                newTxs++
-                logger.debug("x-libeufin-bank transaction with subject 
'${it.subject}' ingested.")
-            }
-            /**
-             * The following block tries to reconcile a previous prepared
-             * (outgoing) payment with the one being iterated over.
-             */
-            if (direction == XLibeufinBankDirection.DEBIT) {
-                val maybePrepared = it.pmtInfId?.let { it1 -> 
getPaymentInitiation(pmtInfId = it1) }
-                if (maybePrepared != null) 
maybePrepared.confirmationTransaction = localTx
-            }
-            // x-libeufin-bank transactions are ALWAYS modeled as reports
-            // in Nexus, because such bank protocol supplier doesn't have
-            // the report vs. statement distinction.  Therefore, we only
-            // consider the last report timestamp.
-            if ((bankAccount.lastStatementCreationTimestamp ?: 0L) < 
maybeTimestamp)
-                bankAccount.lastStatementCreationTimestamp = maybeTimestamp
-        }
-    }
-    return IngestedTransactionsCount(
-        newTransactions = newTxs,
-        downloadedTransactions = txs.transactions.size
-    )
-}
-
-fun XLibeufinBankTransaction.exportCamtDirectionIndicator(): 
CreditDebitIndicator =
-    if (this.direction == XLibeufinBankDirection.CREDIT)
-        CreditDebitIndicator.CRDT else CreditDebitIndicator.DBIT
-
-/**
- * This function transforms an x-libeufin-bank transaction
- * into the JSON representation of CaMt used by Nexus along
- * its processing.  Notably, this helps to stick to one unified
- * type when facades process transactions.
- */
-fun XLibeufinBankTransaction.exportAsCamtModel(): CamtBankAccountEntry =
-    CamtBankAccountEntry(
-        /**
-         * Amount obtained by summing all the transactions accounted
-         * in this report/statement.  Here this field equals the amount of the
-         * _unique_ transaction accounted.
-         */
-        amount = CurrencyAmount(currency = this.currency, value = this.amount),
-        accountServicerRef = this.uid,
-        bankTransactionCode = "Not given",
-        bookingDate = this.date,
-        counterValueAmount = null,
-        creditDebitIndicator = this.exportCamtDirectionIndicator(),
-        currencyExchange = null,
-        entryRef = null,
-        instructedAmount = null,
-        valueDate = null,
-        status = EntryStatus.BOOK, // x-libeufin-bank always/only BOOK.
-        /**
-         * This field accounts for the _unique_ transaction that this
-         * object represents.
-         */
-        batches = listOf(
-            Batch(
-                messageId = null,
-                paymentInformationId = this.uid,
-                batchTransactions = listOf(
-                    BatchTransaction(
-                        amount = CurrencyAmount(
-                            currency = this.currency,
-                            value = this.amount
-                        ),
-                        creditDebitIndicator = 
this.exportCamtDirectionIndicator(),
-                        details = TransactionDetails(
-                            debtor = PartyIdentification(
-                                name = this.debtorName,
-                                countryOfResidence = null,
-                                organizationId = null,
-                                otherId = null,
-                                postalAddress = null,
-                                privateId = null
-                                ),
-                            debtorAccount = CashAccount(
-                                name = null,
-                                currency = this.currency,
-                                iban = this.debtorIban,
-                                otherId = null
-                            ),
-                            debtorAgent = AgentIdentification(
-                                name = null,
-                                bic = this.debtorBic,
-                                clearingSystemCode = null,
-                                clearingSystemMemberId = null,
-                                lei = null,
-                                otherId = null,
-                                postalAddress = null,
-                                proprietaryClearingSystemCode = null
-                            ),
-                            counterValueAmount = null,
-                            currencyExchange = null,
-                            interBankSettlementAmount = null,
-                            proprietaryPurpose = null,
-                            purpose = null,
-                            returnInfo = null,
-                            ultimateCreditor = null,
-                            ultimateDebtor = null,
-                            unstructuredRemittanceInformation = this.subject,
-                            instructedAmount = null,
-                            creditor = PartyIdentification(
-                                name = this.creditorName,
-                                countryOfResidence = null,
-                                organizationId = null,
-                                otherId = null,
-                                postalAddress = null,
-                                privateId = null
-                            ),
-                            creditorAccount = CashAccount(
-                                name = null,
-                                currency = this.currency,
-                                iban = this.creditorIban,
-                                otherId = null
-                            ),
-                            creditorAgent = AgentIdentification(
-                                name = null,
-                                bic = this.creditorBic,
-                                clearingSystemCode = null,
-                                clearingSystemMemberId = null,
-                                lei = null,
-                                otherId = null,
-                                postalAddress = null,
-                                proprietaryClearingSystemCode = null
-                            )
-                        )
-                    )
-                )
-            )
-        )
-    )
\ No newline at end of file
diff --git a/nexus/src/main/resources/logback.xml 
b/nexus/src/main/resources/logback.xml
deleted file mode 100644
index b18b437e..00000000
--- a/nexus/src/main/resources/logback.xml
+++ /dev/null
@@ -1,23 +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="io.netty" level="WARN"/>
-    <logger name="ktor" level="WARN"/>
-    <logger name="Exposed" level="WARN"/>
-    <logger name="tech.libeufin.util" level="DEBUG"/>
-
-    <root level="WARN">
-        <appender-ref ref="STDERR"/>
-    </root>
-
-</configuration>

-- 
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]