libeufin

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

commit 95994e9af317eb115a64341e179c76772afc3055
parent b37353d25cf0e4a8c08fea5ecd6437ccdec550e3
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date:   Thu,  9 Apr 2020 18:39:48 +0200

DB definitions for payments ordered by the exchange.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 52+++++++++++++++++++++++++++++++++++++++++++++-------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 8++++----
Mnexus/src/main/kotlin/tech/libeufin/nexus/taler.kt | 57++++++++++++++++++++++++++++++++++-----------------------
3 files changed, 83 insertions(+), 34 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -10,6 +10,33 @@ import java.sql.Connection const val ID_MAX_LENGTH = 50 +/** + * This table holds the values that exchange gave to issue a payment, + * plus a reference to the prepared pain.001 version of. + */ +object TalerRequestedPayments: LongIdTable() { + val payment = TalerIncomingPayments.reference("payment", Pain001Table) + val requestUId = text("request_uid") + val amount = text("amount") + val exchangeBaseUrl = text("exchange_base_url") + val wtid = text("wtid") + val creditAccount = text("credit_account") +} + +class TalerRequestedPaymentEntity(id: EntityID<Long>) : LongEntity(id) { + var payment by Pain001Entity referencedOn TalerRequestedPayments.payment + 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 +} + +/** + * This table "augments" the information given in the raw payments table, with Taler-related + * ones. It tells if a payment is valid and/or it was refunded already. And moreover, it is + * the table whose ("clean") IDs the exchange will base its history requests on. + */ object TalerIncomingPayments: LongIdTable() { val payment = reference("payment", EbicsRawBankTransactionsTable) val valid = bool("valid") @@ -17,10 +44,15 @@ object TalerIncomingPayments: LongIdTable() { val refunded = bool("refunded").default(false) } -class TalerIncomingPaymentEntry(id: EntityID<Long>) : LongEntity(id) { - companion object : LongEntityClass<TalerIncomingPaymentEntry>(TalerIncomingPayments) { - override fun new(init: TalerIncomingPaymentEntry.() -> Unit): TalerIncomingPaymentEntry { +class TalerIncomingPaymentEntity(id: EntityID<Long>) : LongEntity(id) { + companion object : LongEntityClass<TalerIncomingPaymentEntity>(TalerIncomingPayments) { + override fun new(init: TalerIncomingPaymentEntity.() -> Unit): TalerIncomingPaymentEntity { val newRow = super.new(init) + /** + * In case the exchange asks for all the values strictly lesser than MAX_VALUE, + * it would lose the row whose id == MAX_VALUE. So the check below makes this + * situation impossible by disallowing MAX_VALUE as a id value. + */ if (newRow.id.value == Long.MAX_VALUE) { throw NexusError( HttpStatusCode.InsufficientStorage, "Cannot store rows anymore" @@ -29,7 +61,7 @@ class TalerIncomingPaymentEntry(id: EntityID<Long>) : LongEntity(id) { return newRow } } - var payment by EbicsRawBankTransactionEntry referencedOn TalerIncomingPayments.payment + var payment by EbicsRawBankTransactionEntity referencedOn TalerIncomingPayments.payment var valid by TalerIncomingPayments.valid var refunded by TalerIncomingPayments.refunded } @@ -59,8 +91,8 @@ object EbicsRawBankTransactionsTable : LongIdTable() { val bookingDate = text("bookingDate") } -class EbicsRawBankTransactionEntry(id: EntityID<Long>) : LongEntity(id) { - companion object : LongEntityClass<EbicsRawBankTransactionEntry>(EbicsRawBankTransactionsTable) +class EbicsRawBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) { + companion object : LongEntityClass<EbicsRawBankTransactionEntity>(EbicsRawBankTransactionsTable) var sourceType by EbicsRawBankTransactionsTable.sourceType // C52 or C53 or C54? var sourceFileName by EbicsRawBankTransactionsTable.sourceFileName var unstructuredRemittanceInformation by EbicsRawBankTransactionsTable.unstructuredRemittanceInformation @@ -76,6 +108,12 @@ class EbicsRawBankTransactionEntry(id: EntityID<Long>) : LongEntity(id) { var nexusSubscriber by EbicsSubscriberEntity referencedOn EbicsRawBankTransactionsTable.nexusSubscriber } +/** + * NOTE: every column in this table corresponds to a particular + * value described in the pain.001 official documentation; therefore + * this table is not really suitable to hold custom data (like Taler-related, + * for example) + */ object Pain001Table : IntIdTableWithAmount() { val msgId = long("msgId").uniqueIndex().autoIncrement() val paymentId = long("paymentId") @@ -93,7 +131,7 @@ object Pain001Table : IntIdTableWithAmount() { /* Indicates whether the bank didn't perform the payment: note that * this state can be reached when the payment gets listed in a CRZ - * response OR when the payment doesn's show up in a C52/C53 response + * response OR when the payment doesn't show up in a C52/C53 response */ val invalid = bool("invalid").default(false) } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -314,7 +314,7 @@ fun createPain001document(pain001Entity: Pain001Entity): String { /** * Insert one row in the database, and leaves it marked as non-submitted. */ -fun createPain001entry(entry: Pain001Data, debtorAccountId: String) { +fun createPain001entity(entry: Pain001Data, debtorAccountId: String) { val randomId = Random().nextLong() transaction { Pain001Entity.new { @@ -483,7 +483,7 @@ fun main() { accountInfo.id.value } val pain001data = call.receive<Pain001Data>() - createPain001entry(pain001data, acctid) + createPain001entity(pain001data, acctid) call.respondText( "Payment instructions persisted in DB", ContentType.Text.Plain, HttpStatusCode.OK @@ -630,7 +630,7 @@ fun main() { var ret = "" transaction { val subscriber: EbicsSubscriberEntity = getSubscriberEntityFromId(id) - EbicsRawBankTransactionEntry.find { + EbicsRawBankTransactionEntity.find { (EbicsRawBankTransactionsTable.nexusSubscriber eq subscriber.id.value) and (EbicsRawBankTransactionsTable.sourceType eq "C53") }.forEach { @@ -668,7 +668,7 @@ fun main() { val fileName = it.first val camt53doc = XMLUtil.parseStringIntoDom(it.second) transaction { - EbicsRawBankTransactionEntry.new { + EbicsRawBankTransactionEntity.new { sourceType = "C53" sourceFileName = fileName unstructuredRemittanceInformation = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy") diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt @@ -17,7 +17,6 @@ import org.joda.time.format.DateTimeFormat import tech.libeufin.util.Amount import tech.libeufin.util.CryptoUtil import tech.libeufin.util.toZonedString -import java.util.* import kotlin.math.abs class Taler(app: Route) { @@ -79,7 +78,9 @@ class Taler(app: Route) { val row_id: Long ) - /** Helper data structures. */ + /** + * Helper data structures. + */ data class Payto( val name: String, val iban: String, @@ -90,7 +91,9 @@ class Taler(app: Route) { val amount: Amount ) - /** Helper functions */ + /** + * Helper functions + */ fun parsePayto(paytoUri: String): Payto { // payto://iban/BIC?/IBAN?name=<name> @@ -106,7 +109,7 @@ class Taler(app: Route) { val (currency, number) = match.destructured return AmountWithCurrency(currency, Amount(number)) } - + /** Sort query results in descending order for negative deltas, and ascending otherwise. */ private fun <T : Entity<Long>> SizedIterable<T>.orderTaler(delta: Int): List<T> { return if (delta < 0) { this.sortedByDescending { it.id } @@ -120,10 +123,8 @@ class Taler(app: Route) { private fun parseDate(date: String): DateTime { return DateTime.parse(date, DateTimeFormat.forPattern("YYYY-MM-DD")) } - /** - * Builds the comparison operator for history entries based on the - * sign of 'delta' - */ + + /** Builds the comparison operator for history entries based on the sign of 'delta' */ private fun getComparisonOperator(delta: Int, start: Long): Op<Boolean> { return if (delta < 0) { Expression.build { @@ -135,9 +136,7 @@ class Taler(app: Route) { } } } - /** - * Helper handling 'start' being optional and its dependence on 'delta'. - */ + /** Helper handling 'start' being optional and its dependence on 'delta'. */ private fun handleStartArgument(start: String?, delta: Int): Long { return expectLong(start) ?: if (delta >= 0) { /** @@ -158,16 +157,15 @@ class Taler(app: Route) { /** attaches Taler endpoints to the main Web server */ init { + /** Test-API that creates one new payment addressed to the exchange. */ app.post("/taler/admin/add-incoming") { val exchangeId = authenticateRequest(call.request.headers["Authorization"]) val addIncomingData = call.receive<TalerAdminAddIncoming>() val debtor = parsePayto(addIncomingData.debit_account) val amount = parseAmount(addIncomingData.amount) - - /** Decompose amount and payto fields. */ val (bookingDate, opaque_row_id) = transaction { val exchangeBankAccount = getBankAccountsInfoFromId(exchangeId).first() - val rawPayment = EbicsRawBankTransactionEntry.new { + val rawPayment = EbicsRawBankTransactionEntity.new { sourceFileName = "test" sourceType = "C53" unstructuredRemittanceInformation = addIncomingData.reserve_pub @@ -183,7 +181,7 @@ class Taler(app: Route) { } /** This payment is "valid by default" and will be returned * as soon as the exchange will ask for new payments. */ - val row = TalerIncomingPaymentEntry.new { + val row = TalerIncomingPaymentEntity.new { payment = rawPayment } Pair(rawPayment.bookingDate, row.id.value) @@ -194,6 +192,11 @@ class Taler(app: Route) { )) return@post } + + /** This endpoint triggers the refunding of invalid payments. 'Refunding' + * in this context means that nexus _prepares_ the payment instruction and + * places it into a further table. Eventually, another routine will perform + * all the prepared payments. */ app.post("/ebics/taler/{id}/accounts/{acctid}/refund-invalid-payments") { transaction { val subscriber = expectIdTransaction(call.parameters["id"]) @@ -204,10 +207,10 @@ class Taler(app: Route) { "Such subscriber (${subscriber.id}) can't drive such account (${acctid.id})" ) } - TalerIncomingPaymentEntry.find { + TalerIncomingPaymentEntity.find { TalerIncomingPayments.refunded eq false and (TalerIncomingPayments.valid eq false) }.forEach { - createPain001entry( + createPain001entity( Pain001Data( creditorName = it.payment.debitorName, creditorIban = it.payment.debitorIban, @@ -222,6 +225,11 @@ class Taler(app: Route) { } return@post } + + /** This endpoint triggers the examination of raw incoming payments aimed + * at separating the good payments (those that will lead to a new reserve + * being created), from the invalid payments (those with a invalid subject + * that will soon be refunded.) */ app.post("/ebics/taler/{id}/digest-incoming-transactions") { val id = expectId(call.parameters["id"]) // first find highest ID value of already processed rows. @@ -235,22 +243,22 @@ class Taler(app: Route) { * 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 { + 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. */ - EbicsRawBankTransactionEntry.find { + EbicsRawBankTransactionEntity.find { EbicsRawBankTransactionsTable.creditorIban eq subscriberAccount.iban and (EbicsRawBankTransactionsTable.id.greater(latestId)) }.forEach { if (CryptoUtil.checkValidEddsaPublicKey(it.unstructuredRemittanceInformation)) { - TalerIncomingPaymentEntry.new { + TalerIncomingPaymentEntity.new { payment = it valid = true } } else { - TalerIncomingPaymentEntry.new { + TalerIncomingPaymentEntity.new { payment = it valid = false } @@ -264,6 +272,8 @@ 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. */ app.get("/taler/history/outgoing") { /* sanitize URL arguments */ val subscriberId = authenticateRequest(call.request.headers["Authorization"]) @@ -275,7 +285,7 @@ class Taler(app: Route) { transaction { /** Retrieve all the outgoing payments from the _raw transactions table_ */ val subscriberBankAccount = getBankAccountsInfoFromId(subscriberId) - EbicsRawBankTransactionEntry.find { + EbicsRawBankTransactionEntity.find { EbicsRawBankTransactionsTable.debitorIban eq subscriberBankAccount.first().iban and startCmpOp }.orderTaler(delta).subList(0, abs(delta)).forEach { history.outgoing_transactions.add( @@ -297,6 +307,7 @@ class Taler(app: Route) { ) return@get } + /** Responds only with the valid incoming payments */ app.get("/taler/history/incoming") { val subscriberId = authenticateRequest(call.request.headers["Authorization"]) val delta: Int = expectInt(call.expectUrlParameter("delta")) @@ -305,7 +316,7 @@ class Taler(app: Route) { val startCmpOp = getComparisonOperator(delta, start) transaction { val subscriberBankAccount = getBankAccountsInfoFromId(subscriberId) - TalerIncomingPaymentEntry.find { + TalerIncomingPaymentEntity.find { TalerIncomingPayments.valid eq true and startCmpOp }.orderTaler(delta).subList(0, abs(delta)).forEach { history.incoming_transactions.add(