summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMS <ms@taler.net>2023-10-18 15:22:45 +0200
committerMS <ms@taler.net>2023-10-18 15:22:45 +0200
commitc414263db597bae5a4c29019567b6299cd78553a (patch)
treeb48f9c0ea8ff3a6bfcc3e4af52e0b1bd4d429328
parent70379b90fb1e606fe5fc2f58c85b41c80a686e57 (diff)
downloadlibeufin-c414263db597bae5a4c29019567b6299cd78553a.tar.gz
libeufin-c414263db597bae5a4c29019567b6299cd78553a.tar.bz2
libeufin-c414263db597bae5a4c29019567b6299cd78553a.zip
obsolete nexus gone from master
-rw-r--r--nexus/README29
-rw-r--r--nexus/build.gradle134
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt151
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt98
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt105
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt599
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/DB_helpers.kt123
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt35
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt121
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/JsonLiterals.kt42
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt200
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt172
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt710
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt456
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt437
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt1247
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt286
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt1023
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt79
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt444
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt1182
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt47
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt536
-rw-r--r--nexus/src/main/resources/logback.xml23
24 files changed, 0 insertions, 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>