libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 16e22caf9a4117beb7006ae997981f5fe6dc9495
parent 523a32feb530c0b9392624a794a8984bf9f81c4a
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date:   Wed,  8 Apr 2020 16:53:36 +0200

Completing the incoming history monitor.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 16++++++++++++----
Mnexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 15+++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 19++++++++++++++++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/taler.kt | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 8--------
5 files changed, 181 insertions(+), 36 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -1,12 +1,10 @@ package tech.libeufin.nexus +import io.ktor.http.HttpStatusCode import org.jetbrains.exposed.dao.* import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction -import org.joda.time.DateTime -import tech.libeufin.nexus.EbicsSubscribersTable.entityId -import tech.libeufin.nexus.EbicsSubscribersTable.primaryKey import tech.libeufin.util.IntIdTableWithAmount import java.sql.Connection @@ -20,7 +18,17 @@ object TalerIncomingPayments: LongIdTable() { } class TalerIncomingPaymentEntry(id: EntityID<Long>) : LongEntity(id) { - companion object : LongEntityClass<TalerIncomingPaymentEntry>(TalerIncomingPayments) + companion object : LongEntityClass<TalerIncomingPaymentEntry>(TalerIncomingPayments) { + override fun new(init: TalerIncomingPaymentEntry.() -> Unit): TalerIncomingPaymentEntry { + val newRow = super.new(init) + if (newRow.id.value == Long.MAX_VALUE) { + throw NexusError( + HttpStatusCode.InsufficientStorage, "Cannot store rows anymore" + ) + } + return newRow + } + } var payment by EbicsRawBankTransactionEntry referencedOn TalerIncomingPayments.payment var valid by TalerIncomingPayments.valid var processed by TalerIncomingPayments.processed diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -42,6 +42,21 @@ fun ApplicationCall.expectUrlParameter(name: String): String { ?: throw NexusError(HttpStatusCode.BadRequest, "Parameter '$name' not provided in URI") } +fun expectLong(param: String): Long { + return try { + param.toLong() + } catch (e: Exception) { + throw NexusError(HttpStatusCode.BadRequest,"'$param' is not Long") + } +} + +fun expectLong(param: String?): Long? { + if (param != null) { + return expectLong(param) + } + return null +} + /* Needs a transaction{} block to be called */ fun expectAcctidTransaction(param: String?): EbicsAccountInfoEntity { if (param == null) { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -40,6 +40,7 @@ import io.ktor.routing.post import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty +import org.jetbrains.exposed.sql.SizedIterable import org.jetbrains.exposed.sql.StdOutSqlLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.and @@ -138,6 +139,23 @@ fun getSubscriberDetailsFromBankAccount(bankAccountId: String): EbicsClientSubsc } } +/** + * Given a subscriber id, returns the _list_ of bank accounts associated to it. + * @param id the subscriber id + * @return the query set containing the subscriber's bank accounts + */ +fun getBankAccountsInfoFromId(id: String): SizedIterable<EbicsAccountInfoEntity> { + val list = transaction { + EbicsAccountInfoEntity.find { + EbicsAccountsInfoTable.subscriber eq id + } + } + if (list.empty()) throw NexusError( + HttpStatusCode.NotFound, "This subscriber '$id' did never fetch its own bank accounts, request HTD first." + ) + return list +} + fun getSubscriberDetailsFromId(id: String): EbicsClientSubscriberDetails { return transaction { val subscriber = EbicsSubscriberEntity.findById(id) ?: throw NexusError( @@ -176,7 +194,6 @@ fun getSubscriberDetailsFromId(id: String): EbicsClientSubscriberDetails { * Needs to be called within a transaction block. */ fun createPain001document(pain001Entity: Pain001Entity): String { - /** * Every PAIN.001 document contains at least three IDs: * diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt @@ -3,12 +3,18 @@ package tech.libeufin.nexus import io.ktor.application.call import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.Route import io.ktor.routing.get import io.ktor.routing.post -import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.dao.EntityID +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq import org.jetbrains.exposed.sql.transactions.transaction +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormat +import org.joda.time.format.DateTimeFormatter import tech.libeufin.util.CryptoUtil import tech.libeufin.util.base64ToBytes import java.lang.Exception @@ -115,24 +121,130 @@ class Taler(app: Route) { val row_id: Long ) + private fun SizedIterable<TalerIncomingPaymentEntry>.orderTaler(start: Long): List<TalerIncomingPaymentEntry> { + return if (start < 0) { + this.sortedByDescending { it.id } + } else { + this.sortedBy { it.id } + } + } + /** - * throws error if password is wrong + * Test HTTP basic auth. Throws error if password is wrong + * * @param authorization the Authorization:-header line. + * @return subscriber id */ - private fun authenticateRequest(authorization: String?) { + private fun authenticateRequest(authorization: String?): String { val headerLine = authorization ?: throw NexusError( HttpStatusCode.BadRequest, "Authentication:-header line not found" ) logger.debug("Checking for authorization: $headerLine") - transaction { + val subscriber = transaction { val (user, pass) = extractUserAndHashedPassword(headerLine) EbicsSubscriberEntity.find { EbicsSubscribersTable.id eq user and (EbicsSubscribersTable.password eq SerialBlob(pass)) }.firstOrNull() } ?: throw NexusError(HttpStatusCode.Forbidden, "Wrong password") + return subscriber.id.value + } + + /** + * Implement the Taler wire API transfer method. + */ + private fun transfer(app: Route) { + + } + + private fun getPaytoUri(name: String, iban: String, bic: String): String { + return "payto://$iban/$bic?receiver-name=$name" + } + + /** + * Builds the comparison operator for history entries based on the + * sign of 'delta' + */ + private fun getComparisonOperator(delta: Long, start: Long): Op<Boolean> { + return if (delta < 0) { + Expression.build { + TalerIncomingPayments.id less start + } + } else { + Expression.build { + TalerIncomingPayments.id greater start + } + } + } + + /** + * Helper handling 'start' being optional and its dependence on 'delta'. + */ + private fun handleStartArgument(start: String?, delta: Long): Long { + return expectLong(start) ?: if (delta >= 0) { + /** + * Using -1 as the smallest value, as some DBMS might use 0 and some + * others might use 1 as the smallest row id. + */ + -1 + } else { + /** + * NOTE: the database currently enforces there MAX_VALUE is always + * strictly greater than any row's id in the database. In fact, the + * database throws exception whenever a new row is going to occupy + * the MAX_VALUE with its id. + */ + Long.MAX_VALUE + } } - fun testAuth(app: Route) { + /** + * Respond with ONLY the good transfer made to the exchange. + * A 'good' transfer is one whose subject line is a plausible + * EdDSA public key encoded in Crockford base32. + */ + private fun historyIncoming(app: Route) { + app.get("/taler/history/incoming") { + val subscriberId = authenticateRequest(call.request.headers["Authorization"]) + val delta: Long = expectLong(call.expectUrlParameter("delta")) + val start: Long = handleStartArgument(call.request.queryParameters["start"], delta) + val history = TalerIncomingHistory() + val cmpOp = getComparisonOperator(delta, start) + transaction { + val subscriberBankAccount = getBankAccountsInfoFromId(subscriberId) + TalerIncomingPaymentEntry.find { + TalerIncomingPayments.valid eq true and cmpOp + }.orderTaler(start).forEach { + history.incoming_transactions.add( + TalerIncomingBankTransaction( + date = DateTime.parse(it.payment.bookingDate, DateTimeFormat.forPattern("YYYY-MM-DD")).millis, + row_id = it.id.value, + amount = "${it.payment.currency}:${it.payment.amount}", + reserve_pub = it.payment.unstructuredRemittanceInformation, + debit_account = getPaytoUri( + it.payment.debitorName, it.payment.debitorIban, it.payment.counterpartBic + ), + credit_account = getPaytoUri( + it.payment.creditorName, it.payment.creditorIban, subscriberBankAccount.first().bankCode + ) + ) + ) + } + } + call.respond(history) + return@get + } + } + + /** + * Respond with all the transfers that the exchange made to merchants. + * It can include also those transfers made to reimburse some invalid + * incoming payment. + */ + private fun historyOutgoing(app: Route) { + + } + + private fun testAuth(app: Route) { app.get("/taler/test-auth") { authenticateRequest(call.request.headers["Authorization"]) call.respondText("Authenticated!", ContentType.Text.Plain, HttpStatusCode.OK) @@ -140,27 +252,29 @@ class Taler(app: Route) { } } - fun digest(app: Route) { + private fun digest(app: Route) { app.post("/ebics/taler/{id}/digest-incoming-transactions") { val id = expectId(call.parameters["id"]) // first find highest ID value of already processed rows. transaction { - // avoid re-processing raw payments - val latest = TalerIncomingPaymentEntry.all().sortedByDescending { + /** + * The following query avoids to put a "taler processed" flag-column into + * the raw ebics transactions table. Such table should not contain taler-related + * information. + * + * This latestId value points at the latest id in the _raw transactions table_ + * that was last processed. On the other hand, the "row_id" value that the exchange + * will get along each history element will be the id in the _digested entries table_. + */ + val latestId: Long = TalerIncomingPaymentEntry.all().sortedByDescending { it.payment.id - }.firstOrNull() - - val payments = if (latest == null) { - EbicsRawBankTransactionEntry.find { - EbicsRawBankTransactionsTable.nexusSubscriber eq id - } - } else { - EbicsRawBankTransactionEntry.find { - EbicsRawBankTransactionsTable.id.greater(latest.id) and - (EbicsRawBankTransactionsTable.nexusSubscriber eq id) - } - } - payments.forEach { + }.firstOrNull()?.payment?.id?.value ?: -1 + val subscriberAccount = getBankAccountsInfoFromId(id).first() + /* search for fresh transactions having the exchange IBAN in the creditor field. */ + EbicsRawBankTransactionEntry.find { + EbicsRawBankTransactionsTable.creditorIban eq subscriberAccount.iban and + (EbicsRawBankTransactionsTable.id.greater(latestId)) + }.forEach { if (CryptoUtil.checkValidEddsaPublicKey(it.unstructuredRemittanceInformation)) { TalerIncomingPaymentEntry.new { payment = it @@ -183,8 +297,7 @@ class Taler(app: Route) { } } - fun refund(app: Route) { - + private fun refund(app: Route) { app.post("/ebics/taler/{id}/accounts/{acctid}/refund-invalid-payments") { transaction { val subscriber = expectIdTransaction(call.parameters["id"]) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -221,10 +221,8 @@ object EbicsDownloadTransactionsTable : IdTable<String>() { val receiptReceived = bool("receiptReceived") } - class EbicsDownloadTransactionEntity(id: EntityID<String>) : Entity<String>(id) { companion object : EntityClass<String, EbicsDownloadTransactionEntity>(EbicsDownloadTransactionsTable) - var orderType by EbicsDownloadTransactionsTable.orderType var host by EbicsHostEntity referencedOn EbicsDownloadTransactionsTable.host var subscriber by EbicsSubscriberEntity referencedOn EbicsDownloadTransactionsTable.subscriber @@ -235,7 +233,6 @@ class EbicsDownloadTransactionEntity(id: EntityID<String>) : Entity<String>(id) var receiptReceived by EbicsDownloadTransactionsTable.receiptReceived } - object EbicsUploadTransactionsTable : IdTable<String>() { override val id = text("transactionID").entityId() val orderType = text("orderType") @@ -247,7 +244,6 @@ object EbicsUploadTransactionsTable : IdTable<String>() { val transactionKeyEnc = blob("transactionKeyEnc") } - class EbicsUploadTransactionEntity(id: EntityID<String>) : Entity<String>(id) { companion object : EntityClass<String, EbicsUploadTransactionEntity>(EbicsUploadTransactionsTable) @@ -260,7 +256,6 @@ class EbicsUploadTransactionEntity(id: EntityID<String>) : Entity<String>(id) { var transactionKeyEnc by EbicsUploadTransactionsTable.transactionKeyEnc } - object EbicsOrderSignaturesTable : IntIdTable() { val orderID = text("orderID") val orderType = text("orderType") @@ -270,10 +265,8 @@ object EbicsOrderSignaturesTable : IntIdTable() { val signatureValue = blob("signatureValue") } - class EbicsOrderSignatureEntity(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<EbicsOrderSignatureEntity>(EbicsOrderSignaturesTable) - var orderID by EbicsOrderSignaturesTable.orderID var orderType by EbicsOrderSignaturesTable.orderType var partnerID by EbicsOrderSignaturesTable.partnerID @@ -291,7 +284,6 @@ object EbicsUploadTransactionChunksTable : IdTable<String>() { class EbicsUploadTransactionChunkEntity(id : EntityID<String>): Entity<String>(id) { companion object : EntityClass<String, EbicsUploadTransactionChunkEntity>(EbicsUploadTransactionChunksTable) - var chunkIndex by EbicsUploadTransactionChunksTable.chunkIndex var chunkContent by EbicsUploadTransactionChunksTable.chunkContent }