libeufin

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

commit 13bfc9f8d8eca261e515b4004ab6f24a8b50be1e
parent 793875b7099a3ef8284a4d03cec485bc970333ed
Author: Marcello Stanisci <ms@taler.net>
Date:   Wed, 29 Apr 2020 20:39:14 +0200

Abstracting on "bank account".

Tend to use the triple <iban, bic, holder name>,
instead of the mnemonic label given to bank accounts.

Diffstat:
Mintegration-tests/test-ebics.py | 10+++++++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 26++++++++++++--------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 30+++++++++++++++++++-----------
Mnexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt | 3+++
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 57+++++++++++++++------------------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/taler.kt | 27+++++++++++++++------------
Dnexus/src/test/kotlin/DbTest.kt | 54------------------------------------------------------
Mnexus/src/test/kotlin/PainGeneration.kt | 24++++++++----------------
Mnexus/src/test/kotlin/XPathTest.kt | 4+---
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 3++-
10 files changed, 84 insertions(+), 154 deletions(-)

diff --git a/integration-tests/test-ebics.py b/integration-tests/test-ebics.py @@ -109,7 +109,6 @@ resp = post( assert(resp.status_code == 200) -# FIXME: assert that history is EMPTY at this point! resp = get( "http://localhost:5001/users/{}/history".format(USERNAME) ) @@ -120,5 +119,14 @@ assert( ) #6 Prepare a payment (via pure Nexus service) +resp = post( + "http://localhost:5001/users/{}/prepare-payment".format(USERNAME), + json=dict() +) + +assert(resp.status_code == 200) + + + #7 Execute such payment via EBICS #8 Request history again via EBICS diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -114,12 +114,8 @@ class RawBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) { var nexusUser by NexusUserEntity referencedOn RawBankTransactionsTable.nexusUser var status by RawBankTransactionsTable.status } - /** - * 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) + * Represent a prepare payment. */ object Pain001Table : IntIdTableWithAmount() { val msgId = long("msgId").uniqueIndex().autoIncrement() @@ -127,23 +123,22 @@ object Pain001Table : IntIdTableWithAmount() { val fileDate = long("fileDate") val sum = amount("sum") val currency = varchar("currency", length = 3).default("EUR") - val debtorAccount = text("debtorAccount") val endToEndId = long("EndToEndId") val subject = text("subject") val creditorIban = text("creditorIban") val creditorBic = text("creditorBic") val creditorName = text("creditorName") - + val debitorIban = text("debitorIban") + val debitorBic = text("debitorBic") + val debitorName = text("debitorName").nullable() /* Indicates whether the PAIN message was sent to the bank. */ val submitted = bool("submitted").default(false) - /* 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't show up in a C52/C53 response - */ + * this state can be reached when the payment gets listed in a CRZ + * response OR when the payment doesn't show up in a C52/C53 response */ val invalid = bool("invalid").default(false) + val nexusUser = reference("nexusUser", NexusUsersTable) } - class Pain001Entity(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<Pain001Entity>(Pain001Table) var msgId by Pain001Table.msgId @@ -151,7 +146,9 @@ class Pain001Entity(id: EntityID<Int>) : IntEntity(id) { var date by Pain001Table.fileDate var sum by Pain001Table.sum var currency by Pain001Table.currency - var debtorAccount by Pain001Table.debtorAccount + var debitorIban by Pain001Table.debitorIban + var debitorBic by Pain001Table.debitorBic + var debitorName by Pain001Table.debitorName var endToEndId by Pain001Table.endToEndId var subject by Pain001Table.subject var creditorIban by Pain001Table.creditorIban @@ -159,6 +156,7 @@ class Pain001Entity(id: EntityID<Int>) : IntEntity(id) { var creditorName by Pain001Table.creditorName var submitted by Pain001Table.submitted var invalid by Pain001Table.invalid + var nexusUser by NexusUserEntity referencedOn Pain001Table.nexusUser } /** @@ -206,7 +204,7 @@ class EbicsSubscriberEntity(id: EntityID<Int>) : Entity<Int>(id) { } object NexusUsersTable : IdTable<String>() { - override val id = varchar("id", ID_MAX_LENGTH).entityId().primaryKey() + override val id = varchar("id", ID_MAX_LENGTH).entityId() val ebicsSubscriber = reference("ebicsSubscriber", EbicsSubscribersTable).nullable() val testSubscriber = reference("testSubscriber", EbicsSubscribersTable).nullable() val password = blob("password").nullable() diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -164,6 +164,16 @@ fun createPain001document(pain001Entity: Pain001Entity): String { * we'll assign the SAME id (= the row id) to all the three aforementioned * PAIN id types. */ + val debitorBankAccountLabel = transaction { + val debitorBankAcount = BankAccountEntity.find { + BankAccountsTable.iban eq pain001Entity.debitorIban and + (BankAccountsTable.bankCode eq pain001Entity.debitorBic) + }.firstOrNull() ?: throw NexusError( + HttpStatusCode.NotFound, + "Please download bank accounts details first (HTD)" + ) + debitorBankAcount.id.value + } val s = constructXml(indent = true) { root("Document") { @@ -191,7 +201,7 @@ fun createPain001document(pain001Entity: Pain001Entity): String { text(pain001Entity.sum.toString()) } element("InitgPty/Nm") { - text(pain001Entity.debtorAccount) + text(debitorBankAccountLabel) } } element("PmtInf") { @@ -220,18 +230,13 @@ fun createPain001document(pain001Entity: Pain001Entity): String { text(DateTime(dateMillis).toString("Y-MM-dd")) } element("Dbtr/Nm") { - text(pain001Entity.debtorAccount) + text(debitorBankAccountLabel) } element("DbtrAcct/Id/IBAN") { - text(transaction { - BankAccountEntity.findById(pain001Entity.debtorAccount)?.iban ?: throw NexusError(HttpStatusCode.NotFound,"Debtor IBAN not found in database") - }) + text(pain001Entity.debitorIban) } element("DbtrAgt/FinInstnId/BIC") { - - text(transaction { - BankAccountEntity.findById(pain001Entity.debtorAccount)?.bankCode ?: throw NexusError(HttpStatusCode.NotFound,"Debtor BIC not found in database") - }) + text(pain001Entity.debitorBic) } element("ChrgBr") { text("SLEV") @@ -274,13 +279,15 @@ fun createPain001document(pain001Entity: Pain001Entity): String { * it will be the account whose money will pay the wire transfer being defined * by this pain document. */ -fun createPain001entity(entry: Pain001Data, debtorAccountId: String): Pain001Entity { +fun createPain001entity(entry: Pain001Data, nexusUser: NexusUserEntity): Pain001Entity { val randomId = Random().nextLong() return transaction { Pain001Entity.new { subject = entry.subject sum = entry.sum - debtorAccount = debtorAccountId + debitorIban = entry.debitorIban + debitorBic = entry.debitorBic + debitorName = entry.debitorName creditorName = entry.creditorName creditorBic = entry.creditorBic creditorIban = entry.creditorIban @@ -288,6 +295,7 @@ fun createPain001entity(entry: Pain001Data, debtorAccountId: String): Pain001Ent paymentId = randomId msgId = randomId endToEndId = randomId + this.nexusUser = nexusUser } } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt @@ -139,6 +139,9 @@ data class Pain001Data( val creditorIban: String, val creditorBic: String, val creditorName: String, + val debitorIban: String, + val debitorBic: String, + val debitorName: String?, val sum: Amount, val currency: String = "EUR", val subject: String diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -268,7 +268,7 @@ fun main() { } bankAccountsMap.forEach { Pain001Entity.find { - Pain001Table.debtorAccount eq it.bankAccount.iban + Pain001Table.debitorIban eq it.bankAccount.iban }.forEach { ret.payments.add( RawPayment( @@ -285,20 +285,20 @@ fun main() { call.respond(ret) return@get } - post("/users/{id}/accounts/{acctid}/prepare-payment") { - val acctid = transaction { + post("/users/{id}/accounts/prepare-payment") { + val nexusUser = extractNexusUser(call.parameters["id"]) + transaction { val accountInfo = expectAcctidTransaction(call.parameters["acctid"]) - val nexusUser = extractNexusUser(call.parameters["id"]) if (!userHasRights(nexusUser, accountInfo)) { throw NexusError( HttpStatusCode.BadRequest, "Claimed bank account '${accountInfo.id}' doesn't belong to user '${nexusUser.id.value}'!" ) } - accountInfo.id.value + } val pain001data = call.receive<Pain001Data>() - createPain001entity(pain001data, acctid) + createPain001entity(pain001data, nexusUser) call.respondText( "Payment instructions persisted in DB", ContentType.Text.Plain, HttpStatusCode.OK @@ -329,7 +329,6 @@ fun main() { ) return@get } - /** Associate a EBICS subscriber to the existing user */ post("/ebics/subscribers/{id}") { val nexusUser = extractNexusUser(call.parameters["id"]) @@ -571,17 +570,22 @@ fun main() { /** STATE CHANGES VIA EBICS */ post("/ebics/admin/execute-payments") { - val (paymentRowId, painDoc: String, debtorAccount) = transaction { + val (paymentRowId, painDoc, subscriber) = transaction { val entity = Pain001Entity.find { (Pain001Table.submitted eq false) and (Pain001Table.invalid eq false) }.firstOrNull() ?: throw NexusError(HttpStatusCode.Accepted, reason = "No ready payments found") - Triple(entity.id, createPain001document(entity), entity.debtorAccount) + Triple(entity.id, createPain001document(entity), entity.nexusUser.ebicsSubscriber) + } + if (subscriber == null) { + throw NexusError( + HttpStatusCode.NotFound, + "Ebics subscriber wasn't found for this prepared payment." + ) } logger.debug("Uploading PAIN.001: ${painDoc}") - val subscriberDetails = getSubscriberDetailsFromBankAccount(debtorAccount) doEbicsUploadTransaction( client, - subscriberDetails, + getSubscriberDetailsInternal(subscriber), "CCT", painDoc.toByteArray(Charsets.UTF_8), EbicsStandardOrderParams() @@ -601,37 +605,6 @@ fun main() { ) return@post } - post("/ebics/admin/execute-payments-ccc") { - val (paymentRowId, painDoc: String, debtorAccount) = transaction { - val entity = Pain001Entity.find { - (Pain001Table.submitted eq false) and (Pain001Table.invalid eq false) - }.firstOrNull() ?: throw NexusError(HttpStatusCode.Accepted, reason = "No ready payments found") - Triple(entity.id, createPain001document(entity), entity.debtorAccount) - } - logger.debug("Uploading PAIN.001 via CCC: ${painDoc}") - val subscriberDetails = getSubscriberDetailsFromBankAccount(debtorAccount) - doEbicsUploadTransaction( - client, - subscriberDetails, - "CCC", - listOf(painDoc.toByteArray(Charsets.UTF_8)).zip(), - EbicsStandardOrderParams() - ) - /* flow here == no errors occurred */ - transaction { - val payment = Pain001Entity.findById(paymentRowId) ?: throw NexusError( - HttpStatusCode.InternalServerError, - "Severe internal error: could not find payment in DB after having submitted it to the bank" - ) - payment.submitted = true - } - call.respondText( - "CCC message submitted to the bank", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - return@post - } /** exports keys backup copy */ post("/ebics/subscribers/{id}/backup") { val body = call.receive<EbicsBackupRequestJson>() diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt @@ -236,14 +236,11 @@ class Taler(app: Route) { val transferRequest = call.receive<TalerTransferRequest>() val amountObj = parseAmount(transferRequest.amount) val creditorObj = parsePayto(transferRequest.credit_account) - val opaque_row_id = transaction { val creditorData = parsePayto(transferRequest.credit_account) val exchangeBankAccount = getBankAccountFromNexusUserId(exchangeId) val nexusUser = extractNexusUser(exchangeId) - /** - * Checking the UID has the desired characteristics. - */ + /** Checking the UID has the desired characteristics */ TalerRequestedPaymentEntity.find { TalerRequestedPayments.requestUId eq transferRequest.request_uid }.forEach { @@ -264,11 +261,13 @@ class Taler(app: Route) { creditorBic = creditorData.bic, creditorName = creditorData.name, subject = transferRequest.wtid, - sum = parseAmount(transferRequest.amount).amount + sum = parseAmount(transferRequest.amount).amount, + debitorName = exchangeBankAccount.accountHolder, + debitorBic = exchangeBankAccount.bankCode, + debitorIban = exchangeBankAccount.iban ), - exchangeBankAccount.id.value + nexusUser ) - val rawEbics = if (!isProduction()) { RawBankTransactionEntity.new { sourceFileName = "test" @@ -368,14 +367,15 @@ class Taler(app: Route) { * all the prepared payments. */ app.post("/ebics/taler/{id}/accounts/{acctid}/refund-invalid-payments") { transaction { - val subscriber = getSubscriberEntityFromNexusUserId(call.parameters["id"]) + val nexusUser = extractNexusUser(call.parameters["id"]) val acctid = expectAcctidTransaction(call.parameters["acctid"]) - if (!subscriberHasRights(subscriber, acctid)) { + if (!subscriberHasRights(getEbicsSubscriberFromUser(nexusUser), acctid)) { throw NexusError( HttpStatusCode.Forbidden, - "Such subscriber (${subscriber.id}) can't drive such account (${acctid.id})" + "The requester can't drive such account (${acctid.id})" ) } + val requesterBankAccount = getBankAccountFromNexusUserId(nexusUser.id.value) TalerIncomingPaymentEntity.find { TalerIncomingPayments.refunded eq false and (TalerIncomingPayments.valid eq false) }.forEach { @@ -385,9 +385,12 @@ class Taler(app: Route) { creditorIban = it.payment.debitorIban, creditorBic = it.payment.counterpartBic, sum = calculateRefund(it.payment.amount), - subject = "Taler refund" + subject = "Taler refund", + debitorIban = requesterBankAccount.iban, + debitorBic = requesterBankAccount.bankCode, + debitorName = requesterBankAccount.accountHolder ), - acctid.id.value + nexusUser ) it.refunded = true } diff --git a/nexus/src/test/kotlin/DbTest.kt b/nexus/src/test/kotlin/DbTest.kt @@ -1,53 +0,0 @@ -package tech.libeufin.nexus - -import org.junit.Before -import org.junit.Test - -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.SchemaUtils -import tech.libeufin.util.Amount -import javax.sql.rowset.serial.SerialBlob - - -class DbTest { - - @Before - fun connectAndMakeTables() { - Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") - transaction { - SchemaUtils.create(EbicsSubscribersTable) - SchemaUtils.create(Pain001Table) - } - } - - @Test - fun makeEbicsSubscriber() { - transaction { - EbicsSubscriberEntity.new { - ebicsURL = "ebics url" - hostID = "host" - partnerID = "partner" - userID = "user" - systemID = "system" - signaturePrivateKey = SerialBlob("signturePrivateKey".toByteArray()) - authenticationPrivateKey = SerialBlob("authenticationPrivateKey".toByteArray()) - encryptionPrivateKey = SerialBlob("encryptionPrivateKey".toByteArray()) - } - } - } - - @Test - fun testPain001() { - createPain001entity( - Pain001Data( - creditorBic = "cb", - creditorIban = "ci", - creditorName = "cn", - sum = Amount(2), - subject = "s" - ), - "debtor acctid" - ) - } -} -\ No newline at end of file diff --git a/nexus/src/test/kotlin/PainGeneration.kt b/nexus/src/test/kotlin/PainGeneration.kt @@ -12,28 +12,17 @@ import javax.sql.rowset.serial.SerialBlob class PainTest { - @Before fun prepare() { Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") transaction { - SchemaUtils.create(EbicsSubscribersTable) SchemaUtils.create(BankAccountsTable) SchemaUtils.create(Pain001Table) - EbicsSubscriberEntity.new { - ebicsURL = "ebics url" - hostID = "host" - partnerID = "partner" - userID = "user" - systemID = "system" - signaturePrivateKey = SerialBlob("signturePrivateKey".toByteArray()) - authenticationPrivateKey = SerialBlob("authenticationPrivateKey".toByteArray()) - encryptionPrivateKey = SerialBlob("encryptionPrivateKey".toByteArray()) - } + SchemaUtils.create(NexusUsersTable) BankAccountEntity.new(id = "acctid") { accountHolder = "Account Holder" - iban = "IBAN" - bankCode = "BIC" + iban = "DEBIT IBAN" + bankCode = "DEBIT BIC" } } } @@ -41,9 +30,12 @@ class PainTest { @Test fun testPain001document() { transaction { + val nu = NexusUserEntity.new(id = "mock") { } val pain001Entity = Pain001Entity.new { sum = Amount(1) - debtorAccount = "acctid" + debitorIban = "DEBIT IBAN" + debitorBic = "DEBIT BIC" + debitorName = "DEBIT NAME" subject = "subject line" creditorIban = "CREDIT IBAN" creditorBic = "CREDIT BIC" @@ -52,7 +44,7 @@ class PainTest { msgId = 1 endToEndId = 1 date = DateTime.now().millis - + nexusUser = nu } val s = createPain001document(pain001Entity) println(s) diff --git a/nexus/src/test/kotlin/XPathTest.kt b/nexus/src/test/kotlin/XPathTest.kt @@ -13,10 +13,8 @@ class XPathTest { <node>lorem ipsum</node> </root>""".trimIndent() val doc: Document = XMLUtil.parseStringIntoDom(xml) - val node = XMLUtil.getNodeFromXpath(doc, "/*[local-name()='root']") - assert(node != null) + XMLUtil.getNodeFromXpath(doc, "/*[local-name()='root']") val text = XMLUtil.getStringFromXpath(doc, "//*[local-name()='node']") - assert(text != null) println(text) } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -256,7 +256,8 @@ fun dbCreateTables() { EbicsDownloadTransactionsTable, EbicsUploadTransactionsTable, EbicsUploadTransactionChunksTable, - EbicsOrderSignaturesTable + EbicsOrderSignaturesTable, + PaymentsTable ) } } \ No newline at end of file