libeufin

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

commit 1c89426792953a838491b4592a14cc38849e43ed
parent 088c518e0ed7d9e5f448035fdc41426c6887b405
Author: MS <ms@taler.net>
Date:   Wed,  3 Jun 2020 18:59:58 +0200

Advancing for the exchange-nexus test.

Sandbox: fill CAMT values according to actual data received in payments.
Nexus: fix the launching of HTTP requests (namely to obtain C53) in the background.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 40+++++++++++++++++++++++++++-------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/taler.kt | 64++++++++++++++++++++++++++++++++++++++--------------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 8++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 37++++++++++++++++++++++++-------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 10+++++++++-
Mutil/src/main/kotlin/JSON.kt | 4++++
6 files changed, 110 insertions(+), 53 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -56,8 +56,7 @@ import io.ktor.server.netty.Netty import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.jvm.javaio.toByteReadChannel import io.ktor.utils.io.jvm.javaio.toInputStream -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.coroutines.time.delay import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.statements.api.ExposedBlob @@ -75,6 +74,7 @@ import java.util.* import java.util.zip.InflaterInputStream import javax.crypto.EncryptedPrivateKeyInfo import java.time.LocalDateTime +import kotlin.coroutines.CoroutineContext data class NexusError(val statusCode: HttpStatusCode, val reason: String) : Exception("${reason} (HTTP status $statusCode)") @@ -258,21 +258,37 @@ fun ApplicationRequest.hasBody(): Boolean { } return false } -suspend fun schedulePeriodicWork(coroutineScope: CoroutineScope) { - while (true) { - delay(Duration.ofMillis(100)) - downloadFacadesTransactions(coroutineScope) - ingestTalerTransactions() + +fun schedulePeriodicWork() { + GlobalScope.launch { + while (true) { + logger.debug("Outer background job") + try { + delay(Duration.ofSeconds(1)) + downloadFacadesTransactions(this) + ingestTalerTransactions() + } catch (e: Exception) { + logger.info("==== Background job exception ====\n${e.message}======") + } + } } } /** Crawls all the facades, and requests history for each of its creators. */ -suspend fun downloadFacadesTransactions(coroutineScope: CoroutineScope) { +suspend fun downloadFacadesTransactions(myScope: CoroutineScope) { val httpClient = HttpClient() transaction { FacadeEntity.all().forEach { - coroutineScope.launch { - fetchTransactionsInternal(httpClient, it.creator, it.config.bankAccount, CollectedTransaction()) + logger.debug( + "Fetching history for facade: ${it.id.value}, bank account: ${it.config.bankAccount}" + ) + runBlocking { + fetchTransactionsInternal( + httpClient, + it.creator, + it.config.bankAccount, + CollectedTransaction(null, null, null) + ) } } } @@ -344,9 +360,6 @@ fun serverMain(dbName: String) { expectSuccess = false // this way, it does not throw exceptions on != 200 responses. } val server = embeddedServer(Netty, port = 5001) { - launch { - schedulePeriodicWork(this) - } install(CallLogging) { this.level = Level.DEBUG this.logger = tech.libeufin.nexus.logger @@ -413,6 +426,7 @@ fun serverMain(dbName: String) { return@intercept } + schedulePeriodicWork() routing { /** * Shows information about the requesting user. diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt @@ -3,9 +3,12 @@ package tech.libeufin.nexus import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.ktor.application.ApplicationCall import io.ktor.application.call +import io.ktor.client.HttpClient +import io.ktor.client.request.post import io.ktor.content.TextContent import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType import io.ktor.request.receive import io.ktor.response.respond import io.ktor.response.respondText @@ -226,7 +229,7 @@ fun getFacadeBankAccount(nexusUser: NexusUserEntity): NexusBankAccountEntity { } // /taler/transfer -suspend fun talerTransfer(call: ApplicationCall): Unit { +suspend fun talerTransfer(call: ApplicationCall) { val transferRequest = call.receive<TalerTransferRequest>() val amountObj = parseAmount(transferRequest.amount) val creditorObj = parsePayto(transferRequest.credit_account) @@ -260,20 +263,6 @@ suspend fun talerTransfer(call: ApplicationCall): Unit { ), exchangeBankAccount ) - val rawEbics = if (!isProduction()) { - RawBankTransactionEntity.new { - unstructuredRemittanceInformation = transferRequest.wtid - transactionType = "DBIT" - currency = amountObj.currency - this.amount = amountObj.amount.toPlainString() - counterpartBic = creditorObj.bic - counterpartIban = creditorObj.iban - counterpartName = creditorObj.name - bankAccount = exchangeBankAccount - bookingDate = System.currentTimeMillis() - status = "BOOK" - } - } else null val row = TalerRequestedPaymentEntity.new { preparedPayment = pain001 // not really used/needed, just here to silence warnings exchangeBaseUrl = transferRequest.exchange_base_url @@ -281,7 +270,6 @@ suspend fun talerTransfer(call: ApplicationCall): Unit { amount = transferRequest.amount wtid = transferRequest.wtid creditAccount = transferRequest.credit_account - rawConfirmed = rawEbics } row.id.value } @@ -307,25 +295,49 @@ suspend fun talerTransfer(call: ApplicationCall): Unit { suspend fun talerAddIncoming(call: ApplicationCall): Unit { val addIncomingData = call.receive<TalerAdminAddIncoming>() val debtor = parsePayto(addIncomingData.debit_account) - val amount = parseAmount(addIncomingData.amount) - - val myLastSeenRawPayment = transaction { + val res = transaction { val facadeID = expectNonNull(call.parameters["fcid"]) val facade = FacadeEntity.findById(facadeID) ?: throw NexusError( - HttpStatusCode.NotFound, "Could not find facade" + HttpStatusCode.NotFound, "Could not find facade '$facadeID'" + ) + val facadeBankAccount = NexusBankAccountEntity.findById(facade.config.bankAccount) ?: throw NexusError( + HttpStatusCode.NotFound, + "Such bank account '${facade.config.bankAccount}' wasn't found for facade '$facadeID'" ) - facade.highestSeenMsgID + return@transaction object { + val facadeLastSeen = facade.highestSeenMsgID + val facadeIban = facadeBankAccount.iban + val facadeBic = facadeBankAccount.bankCode + val facadeHolderName = facadeBankAccount.accountHolder + } } + val httpClient = HttpClient() + /** forward the payment information to the sandbox. */ + httpClient.post<String>( + urlString = "http://localhost:5000/admin/payments", + block = { + /** FIXME: ideally Jackson should define such request body. */ + this.body = """{ + "creditorIban": "${res.facadeIban}", + "creditorBic": "${res.facadeBic}", + "creditorName": "${res.facadeHolderName}", + "debitorIban": "${debtor.iban}", + "debitorBic": "${debtor.bic}", + "debitorName": "${debtor.name}", + "amount": "${addIncomingData.amount}", + "subject": "${addIncomingData.reserve_pub}" + }""".trimIndent() + contentType(ContentType.Application.Json) + } + ) return call.respond( TextContent( customConverter( TalerAddIncomingResponse( timestamp = GnunetTimestamp( - // warning: this value might need to come from a real last-seen payment. - // FIXME(dold): I don't understand the comment above ^^. System.currentTimeMillis() ), - row_id = myLastSeenRawPayment + row_id = res.facadeLastSeen ) ), ContentType.Application.Json @@ -343,6 +355,7 @@ suspend fun talerAddIncoming(call: ApplicationCall): Unit { */ fun ingestTalerTransactions() { fun ingest(subscriberAccount: NexusBankAccountEntity, facade: FacadeEntity) { + logger.debug("Ingesting transactions for Taler facade: ${facade.id.value}") var lastId = facade.highestSeenMsgID RawBankTransactionEntity.find { /** Those with exchange bank account involved */ @@ -510,4 +523,4 @@ fun talerFacadeRoutes(route: Route) { call.respondText("Hello, this is Taler Facade") return@get } -} -\ No newline at end of file +} diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -251,7 +251,11 @@ class EbicsUploadTransactionChunkEntity(id: EntityID<String>) : Entity<String>(i */ object PaymentsTable : IntIdTable() { val creditorIban = text("creditorIban") + val creditorBic = text("creditorBic") + val creditorName = text("creditorName") val debitorIban = text("debitorIban") + val debitorBic = text("debitorBic") + val debitorName = text("debitorName") val subject = text("subject") val amount = text("amount") val date = long("date") @@ -261,7 +265,11 @@ class PaymentEntity(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<PaymentEntity>(PaymentsTable) var creditorIban by PaymentsTable.creditorIban + var creditorBic by PaymentsTable.creditorBic + var creditorName by PaymentsTable.creditorName var debitorIban by PaymentsTable.debitorIban + var debitorBic by PaymentsTable.debitorBic + var debitorName by PaymentsTable.debitorName var subject by PaymentsTable.subject var amount by PaymentsTable.amount diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -140,7 +140,7 @@ fun <T>expectNonNull(x: T?): T { * words, the camt constructor does creates always only one "Ntry" * node. */ -fun buildCamtString(type: Int, history: MutableList<RawPayment>): MutableList<String> { +fun buildCamtString(type: Int, subscriberIban: String, history: MutableList<RawPayment>): MutableList<String> { /** * ID types required: * @@ -158,7 +158,7 @@ fun buildCamtString(type: Int, history: MutableList<RawPayment>): MutableList<St val dashedDate = expectNonNull(it.date) val now = LocalDateTime.now() val zonedDateTime = now.toZonedString() - + val amount = parseAmount(it.amount) ret.add( constructXml(indent = true) { root("Document") { @@ -201,7 +201,7 @@ fun buildCamtString(type: Int, history: MutableList<RawPayment>): MutableList<St element("Acct") { // mandatory account identifier element("Id/IBAN") { - text(it.debitorIban) + text(subscriberIban) } element("Ccy") { text("EUR") @@ -240,7 +240,7 @@ fun buildCamtString(type: Int, history: MutableList<RawPayment>): MutableList<St text(Amount(0).toPlainString()) } element("CdtDbtInd") { - text("DBIT") + text("UNUSED") } element("Dt/Dt") { // date of this balance @@ -260,20 +260,22 @@ fun buildCamtString(type: Int, history: MutableList<RawPayment>): MutableList<St } element("CdtDbtInd") { // CRDT or DBIT here - text("DBIT") + text("UNUSED") } element("Dt/Dt") { text(dashedDate) } } - element("Ntry") { element("Amt") { - attribute("Ccy", "EUR") - text(it.amount) + attribute("Ccy", amount.currency) + text(amount.amount.toString()) } element("CdtDbtInd") { - text("DBIT") + text( + if (subscriberIban.equals(it.creditorIban)) + "CRDT" else "DBIT" + ) } element("Sts") { /* Status of the entry (see 2.4.2.15.5 from the ISO20022 reference document.) @@ -355,13 +357,13 @@ fun buildCamtString(type: Int, history: MutableList<RawPayment>): MutableList<St } element("RltdPties") { element("Dbtr/Nm") { - text("Debitor Name") + text(it.debitorName) } element("DbtrAcct/Id/IBAN") { text(it.debitorIban) } element("Cdtr/Nm") { - text("Creditor Name") + text(it.creditorName) } element("CdtrAcct/Id/IBAN") { text(it.creditorIban) @@ -369,7 +371,10 @@ fun buildCamtString(type: Int, history: MutableList<RawPayment>): MutableList<St } element("RltdAgts") { element("CdtrAgt/FinInstnId/BIC") { - text("Creditor Bic") + text( + if (subscriberIban.equals(it.creditorIban)) + it.debitorBic else it.creditorBic + ) } } element("RmtInf/Ustrd") { @@ -411,6 +416,7 @@ private fun constructCamtResponse( val history = mutableListOf<RawPayment>() val bankAccount = getBankAccountFromSubscriber(subscriber) transaction { + logger.debug("Querying transactions involving: ${bankAccount.iban}") PaymentEntity.find { PaymentsTable.creditorIban eq bankAccount.iban or (PaymentsTable.debitorIban eq bankAccount.iban) @@ -423,7 +429,11 @@ private fun constructCamtResponse( RawPayment( subject = it.subject, creditorIban = it.creditorIban, + creditorBic = it.creditorBic, + creditorName = it.creditorName, debitorIban = it.debitorIban, + debitorBic = it.debitorBic, + debitorName = it.debitorName, date = importDateFromMillis(it.date).toDashedDate(), amount = it.amount ) @@ -431,7 +441,7 @@ private fun constructCamtResponse( } history } - return buildCamtString(type, history) + return buildCamtString(type, bankAccount.iban, history) } private fun handleEbicsTSD(requestContext: RequestContext): ByteArray { @@ -471,6 +481,7 @@ private fun handleCct(paymentRequest: String) { } private fun handleEbicsC53(requestContext: RequestContext): ByteArray { + logger.debug("Handling C53 request") val camt = constructCamtResponse( 53, requestContext.requestObject.header, diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -171,7 +171,11 @@ fun main() { debitorIban = it.debitorIban, subject = it.subject, date = it.date.toHttpDateString(), - amount = it.amount + amount = it.amount, + creditorBic = it.creditorBic, + creditorName = it.creditorName, + debitorBic = it.debitorBic, + debitorName = it.debitorName ) ) } @@ -188,7 +192,11 @@ fun main() { transaction { PaymentEntity.new { creditorIban = body.creditorIban + creditorBic = body.creditorBic + creditorName = body.creditorName debitorIban = body.debitorIban + debitorBic = body.debitorBic + debitorName = body.debitorName subject = body.subject amount = body.amount date = Instant.now().toEpochMilli() diff --git a/util/src/main/kotlin/JSON.kt b/util/src/main/kotlin/JSON.kt @@ -7,7 +7,11 @@ package tech.libeufin.util */ data class RawPayment( val creditorIban: String, + val creditorBic: String, + val creditorName: String, val debitorIban: String, + val debitorBic: String, + val debitorName: String, val amount: String, val subject: String, val date: String?