libeufin

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

commit 70763ba07ef2e76031626010a87c1b9bc9e5e57b
parent f54438f79232c106899bdb53e27f8084ef6d21bb
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date:   Mon, 13 Apr 2020 18:32:58 +0200

Wire gateway API.

Outgoing payments are counted as those with a non-null
reference to the raw EBICS table.  Link between camt.053
entries and Taler-requested payments is missing.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 16+++++++++++-----
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 2--
Mnexus/src/main/kotlin/tech/libeufin/nexus/taler.kt | 65+++++++++++++++++++++++++++++++++++++++--------------------------
3 files changed, 50 insertions(+), 33 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -23,16 +23,22 @@ object TalerRequestedPayments: LongIdTable() { val exchangeBaseUrl = text("exchange_base_url") val wtid = text("wtid") val creditAccount = text("credit_account") + /** + * This column gets a value only after the bank acknowledges the payment via + * a camt.05x entry. The "crunch" logic is responsible for assigning such value. + */ + val rawConfirmed = reference("raw_confirmed", EbicsRawBankTransactionsTable).nullable() } class TalerRequestedPaymentEntity(id: EntityID<Long>) : LongEntity(id) { - companion object : LongEntityClass<TalerRequestedPaymentEntity>(TalerIncomingPayments) + companion object : LongEntityClass<TalerRequestedPaymentEntity>(TalerRequestedPayments) var preparedPayment by Pain001Entity referencedOn TalerRequestedPayments.preparedPayment var requestUId by TalerRequestedPayments.requestUId var amount by TalerRequestedPayments.amount var exchangeBaseUrl by TalerRequestedPayments.exchangeBaseUrl var wtid by TalerRequestedPayments.wtid var creditAccount by TalerRequestedPayments.creditAccount + var rawConfirmed by EbicsRawBankTransactionEntity optionalReferencedOn TalerRequestedPayments.rawConfirmed } /** @@ -47,6 +53,10 @@ object TalerIncomingPayments: LongIdTable() { val refunded = bool("refunded").default(false) } +fun LongEntityClass<*>.getLast(): Long { + return this.all().maxBy { it.id }?.id?.value ?: -1 +} + class TalerIncomingPaymentEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<TalerIncomingPaymentEntity>(TalerIncomingPayments) { override fun new(init: TalerIncomingPaymentEntity.() -> Unit): TalerIncomingPaymentEntity { @@ -88,8 +98,6 @@ object EbicsRawBankTransactionsTable : LongIdTable() { val counterpartBic = text("counterpartBic") val bookingDate = text("bookingDate") val status = text("status") // BOOK, .. - val servicerCode = text("servicerCode").nullable() /* "internal" code given by the bank */ - val proprietaryCode = text("proprietaryCode") /* code given by the DK */ } class EbicsRawBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) { @@ -107,8 +115,6 @@ class EbicsRawBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) { var bookingDate by EbicsRawBankTransactionsTable.bookingDate var nexusSubscriber by EbicsSubscriberEntity referencedOn EbicsRawBankTransactionsTable.nexusSubscriber var status by EbicsRawBankTransactionsTable.status - var servicerCode by EbicsRawBankTransactionsTable.servicerCode - var proprietaryCode by EbicsRawBankTransactionsTable.proprietaryCode } /** diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -677,8 +677,6 @@ fun main() { currency = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy") amount = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']") status = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Sts']") - servicerCode = camt53doc.pickStringNullable("//*[local-name()='Ntry']//*[local-name()='AcctSvcrRef']") - proprietaryCode = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='BkTxCd']/*[local-name()='Prtry']/*[local-name()='Cd']") bookingDate = camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']") nexusSubscriber = getSubscriberEntityFromId(id) creditorName = diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt @@ -118,7 +118,10 @@ class Taler(app: Route) { } } private fun getPaytoUri(name: String, iban: String, bic: String): String { - return "payto://$iban/$bic?receiver-name=$name" + return "payto://iban/$iban/$bic?receiver-name=$name" + } + private fun getPaytoUri(iban: String, bic: String): String { + return "payto://iban/$iban/$bic" } private fun parseDate(date: String): DateTime { return DateTime.parse(date, DateTimeFormat.forPattern("YYYY-MM-DD")) @@ -235,8 +238,6 @@ class Taler(app: Route) { counterpartBic = debtor.bic bookingDate = DateTime.now().toZonedString() status = "BOOK" - servicerCode = "test-0" - proprietaryCode = "test-0" } /** This payment is "valid by default" and will be returned * as soon as the exchange will ask for new payments. */ @@ -293,23 +294,18 @@ class Taler(app: Route) { val id = expectId(call.parameters["id"]) // first find highest ID value of already processed rows. transaction { + val subscriberAccount = getBankAccountsInfoFromId(id).first() + /** - * 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 here. Note, the "row_id" value that the exchange - * will get along each history element will be the id in the _crunched entries table_. + * Search for fresh INCOMING transactions having a BOOK status. Cancellations and + * other status changes will (1) be _appended_ to the payment history, and (2) be + * handled _independently_ another dedicated routine. */ - val latestId: Long = TalerIncomingPaymentEntity.all().sortedByDescending { - it.payment.id - }.firstOrNull()?.payment?.id?.value ?: -1 - val subscriberAccount = getBankAccountsInfoFromId(id).first() - /* search for fresh transactions having the exchange IBAN in the creditor field. */ + val latestIncomingPaymentId: Long = TalerIncomingPaymentEntity.getLast() EbicsRawBankTransactionEntity.find { EbicsRawBankTransactionsTable.creditorIban eq subscriberAccount.iban and - (EbicsRawBankTransactionsTable.id.greater(latestId)) + (EbicsRawBankTransactionsTable.status eq "BOOK") and + (EbicsRawBankTransactionsTable.id.greater(latestIncomingPaymentId)) }.forEach { if (CryptoUtil.checkValidEddsaPublicKey(it.unstructuredRemittanceInformation)) { TalerIncomingPaymentEntity.new { @@ -323,6 +319,19 @@ class Taler(app: Route) { } } } + + /** + * Search for fresh OUTGOING transactions acknowledged by the bank. As well + * searching only for BOOKed transactions, even though status changes should + * be really unexpected here. + */ + val latestOutgoingPaymentId = TalerRequestedPaymentEntity.getLast() + EbicsRawBankTransactionEntity.find { + EbicsRawBankTransactionsTable.id greater latestOutgoingPaymentId and + (EbicsRawBankTransactionsTable.status eq "BOOK") + }.forEach { + + } } call.respondText ( "New raw payments Taler-processed", @@ -332,7 +341,9 @@ class Taler(app: Route) { return@post } /** Responds only with the payments that the EXCHANGE made. Typically to - * merchants but possibly to refund invalid incoming payments. */ + * merchants but possibly to refund invalid incoming payments. A payment is + * counted only if was once confirmed by the bank. + */ app.get("/taler/history/outgoing") { /* sanitize URL arguments */ val subscriberId = authenticateRequest(call.request.headers["Authorization"]) @@ -342,19 +353,21 @@ class Taler(app: Route) { /* retrieve database elements */ val history = TalerOutgoingHistory() transaction { - /** Retrieve all the outgoing payments from the _raw transactions table_ */ - val subscriberBankAccount = getBankAccountsInfoFromId(subscriberId) - EbicsRawBankTransactionEntity.find { - EbicsRawBankTransactionsTable.debitorIban eq subscriberBankAccount.first().iban and startCmpOp + /** Retrieve all the outgoing payments from the _clean Taler outgoing table_ */ + val subscriberBankAccount = getBankAccountsInfoFromId(subscriberId).first() + TalerRequestedPaymentEntity.find { + TalerRequestedPayments.rawConfirmed.isNotNull() and startCmpOp }.orderTaler(delta).subList(0, abs(delta)).forEach { history.outgoing_transactions.add( TalerOutgoingBankTransaction( row_id = it.id.value, - amount = "${it.currency}:${it.amount}", - wtid = it.unstructuredRemittanceInformation, - date = parseDate(it.bookingDate).millis / 1000, - credit_account = it.creditorIban, - debit_account = it.debitorIban, + amount = it.amount, + wtid = it.wtid, + date = parseDate(it.rawConfirmed?.bookingDate ?: throw NexusError( + HttpStatusCode.InternalServerError, "Null value met after check, VERY strange.") + ).millis / 1000, + credit_account = it.creditAccount, + debit_account = getPaytoUri(subscriberBankAccount.iban, subscriberBankAccount.bankCode), exchange_base_url = "FIXME-to-request-along-subscriber-registration" ) )