libeufin

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

commit 4bc5f38f571a45d427f73813ec3846bf59413afa
parent 06452b9adc4d149bdb1532a3ea3160909eb51c9a
Author: MS <ms@taler.net>
Date:   Thu,  6 Jul 2023 11:21:28 +0200

EBICS 3 fixes.

Removing one stale reference to schema H004 in the
new H005 <ebicsResponse>, and avoiding to validate
every response that arrives from the bank.

Diffstat:
MMakefile | 10+++++++---
Mcli/bin/libeufin-cli | 1-
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt | 1+
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 24+++++++++++++++++++-----
Mnexus/src/test/kotlin/PostFinance.kt | 65++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mutil/src/main/kotlin/Ebics.kt | 7++++++-
Mutil/src/main/kotlin/ebics_h005/Ebics3Response.kt | 8+++-----
7 files changed, 88 insertions(+), 28 deletions(-)

diff --git a/Makefile b/Makefile @@ -68,6 +68,10 @@ check-cli: @cd ./cli/tests && ./circuit_test.sh @cd ./cli/tests && ./debit_test.sh -.PHONY: pofi -pofi: - @./gradlew -q :nexus:pofi +.PHONY: pofi-get +pofi-get: + @./gradlew -q :nexus:pofi --args="download" # --args="arg1 arg2 .." + +.PHONY: pofi-post +pofi-post: + @./gradlew -q :nexus:pofi --args="upload" diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli @@ -574,7 +574,6 @@ def new_ebics_connection( check_response_status(resp) - @connections.command(help="Initialize the bank connection.") @click.argument("connection-name") @click.pass_obj diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt @@ -169,6 +169,7 @@ suspend fun doEbicsDownloadTransaction( return EbicsDownloadEmptyResult() } else -> { + println("Bank raw response: $initResponseStr") logger.error( "Bank-technical error at init phase: ${initResponse.bankReturnCode}" + ", for fetching level ${fetchSpec.originalLevel} and transaction ID $transactionID." diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -99,7 +99,8 @@ private fun validateAndStoreCamt( bankConnectionId: String, camt: String, fetchLevel: FetchLevel, - transactionID: String? = null // the EBICS transaction that carried this camt. + transactionID: String? = null, // the EBICS transaction that carried this camt. + validateBankContent: Boolean = false ) { val camtDoc = try { XMLUtil.parseStringIntoDom(camt) @@ -107,8 +108,10 @@ private fun validateAndStoreCamt( catch (e: Exception) { throw badGateway("Could not parse camt document from EBICS transaction $transactionID") } - if (!XMLUtil.validateFromDom(camtDoc)) + if (validateBankContent && !XMLUtil.validateFromDom(camtDoc)) { + logger.error("This document didn't validate: $camt") throw badGateway("Camt document from EBICS transaction $transactionID is invalid") + } val msgId = camtDoc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId") logger.info("Camt document '$msgId' received via $fetchLevel.") @@ -191,7 +194,7 @@ private suspend fun fetchEbicsTransactions( ) } catch (e: EbicsProtocolError) { /** - * Although given a error type, a empty transactions list does + * Although given a error type, an empty transactions list does * not mean anything wrong. */ if (e.ebicsTechnicalCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) { @@ -498,7 +501,17 @@ private fun getStatementSpecAfterDialect(dialect: String? = null, p: EbicsOrderP "pf" -> EbicsFetchSpec( orderType = "Z53", orderParams = p, - ebics3Service = null, + ebics3Service = Ebics3Request.OrderDetails.Service().apply { + serviceName = "EOP" + messageName = Ebics3Request.OrderDetails.Service.MessageName().apply { + value = "camt.053" + version = "04" + } + scope = "CH" + container = Ebics3Request.OrderDetails.Service.Container().apply { + containerType = "ZIP" + } + }, originalLevel = FetchLevel.STATEMENT ) else -> EbicsFetchSpec( @@ -519,7 +532,7 @@ private fun getNotificationSpecAfterDialect(dialect: String? = null, p: EbicsOrd serviceName = "REP" messageName = Ebics3Request.OrderDetails.Service.MessageName().apply { value = "camt.054" - version = "04" + version = "08" } scope = "CH" container = Ebics3Request.OrderDetails.Service.Container().apply { @@ -739,6 +752,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { ) } catch (e: Exception) { logger.warn("Fetching transactions (${spec.originalLevel}) excepted: ${e.message}.") + e.printStackTrace() errors.add(e) } } diff --git a/nexus/src/test/kotlin/PostFinance.kt b/nexus/src/test/kotlin/PostFinance.kt @@ -1,4 +1,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.github.ajalt.clikt.core.* +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option import io.ktor.client.* import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.transactions.transaction @@ -11,13 +14,13 @@ import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails import tech.libeufin.nexus.getConnectionPlugin import tech.libeufin.nexus.getNexusUser import tech.libeufin.nexus.server.* -import tech.libeufin.util.EbicsStandardOrderParams import tech.libeufin.util.ebics_h005.Ebics3Request import java.io.BufferedReader import java.io.File +import kotlin.system.exitProcess // Asks a camt.054 to the bank. -private fun downloadPayment() { +private fun downloadPayments() { val httpClient = HttpClient() runBlocking { fetchBankAccountTransactions( @@ -31,14 +34,19 @@ private fun downloadPayment() { } } -/* Simulates one incoming payment for the test platorm's bank account. +/* Simulates one incoming payment for the 'payee' argument. + * It pays the test platform's bank account if none is found. * The QRR format is NOT used in Taler, it is just convenient. * */ -private fun uploadQrrPayment() { +private fun uploadQrrPayment(maybePayee: String? = null) { + val payee = if (maybePayee == null) { + val localAccount = getBankAccount("foo") + localAccount.iban + } else maybePayee val httpClient = HttpClient() val qrr = """ Product;Channel;Account;Currency;Amount;Reference;Name;Street;Number;Postcode;City;Country;DebtorAddressLine;DebtorAddressLine;DebtorAccount;ReferenceType;UltimateDebtorName;UltimateDebtorStreet;UltimateDebtorNumber;UltimateDebtorPostcode;UltimateDebtorTownName;UltimateDebtorCountry;UltimateDebtorAddressLine;UltimateDebtorAddressLine;RemittanceInformationText - QRR;PO;CH9789144829733648596;CHF;33;;D009;Musterstrasse;1;1111;Musterstadt;CH;;;;NON;D009;Musterstrasse;1;1111;Musterstadt;CH;;;Taler-Demo + QRR;PO;$payee;CHF;33;;D009;Musterstrasse;1;1111;Musterstadt;CH;;;;NON;D009;Musterstrasse;1;1111;Musterstadt;CH;;;Taler-Demo """.trimIndent() runBlocking { doEbicsUploadTransaction( @@ -69,16 +77,19 @@ private fun uploadQrrPayment() { * by the sender. Hence, the sender can itself ensure the EndToEndId * uniqueness. */ -private fun uploadPain001Payment() { +private fun uploadPain001Payment( + subject: String, + creditorIban: String = "CH9300762011623852957" // random creditor +) { transaction { addPaymentInitiation( Pain001Data( - creditorIban = "CH9300762011623852957", + creditorIban = creditorIban, creditorBic = "POFICHBEXXX", creditorName = "Muster Frau", sum = "2", currency = "CHF", - subject = "Muster Zahlung 0", + subject = subject, endToEndId = "Zufall" ), getBankAccount("foo") @@ -89,7 +100,33 @@ private fun uploadPain001Payment() { runBlocking { ebicsConn.submitPaymentInitiation(httpClient, 1L) } } -fun main() { +class PostFinanceCommand : CliktCommand() { + private val myIban by option( + help = "IBAN as assigned by the PostFinance test platform." + ).default("CH9789144829733648596") + override fun run() { prepare(myIban) } +} +class Download : CliktCommand("Download the latest camt.054 from the bank") { + // Ask 'notification' to the bank. + override fun run() { + // uploadPain001Payment("auto") + downloadPayments() + } +} + +class Upload : CliktCommand("Upload a pain.001 to the bank") { + private val subject by option(help = "Payment subject").default("Muster Zahlung") + override fun run() { uploadPain001Payment(subject) } +} + +class GenIncoming : CliktCommand("Uploads a CSV document to create one incoming payment") { + override fun run() { + val bankAccount = getBankAccount("foo") + uploadQrrPayment(bankAccount.iban) + } +} + +private fun prepare(iban: String) { // Loads EBICS subscriber's keys from disk. // The keys should be found under libeufin-internal.git/convenience/ val bufferedReader: BufferedReader = File("/tmp/pofi.json").bufferedReader() @@ -108,11 +145,13 @@ fun main() { accessDataJson ) val fooBankAccount = getBankAccount("foo") + // Hooks the PoFi details to the local bank account. + // No need to run the canonical steps (creating account, downloading bank accounts, ..) fooBankAccount.defaultBankConnection = getBankConnection("postfinance") - fooBankAccount.iban = "CH9789144829733648596" + fooBankAccount.iban = iban } } - uploadQrrPayment() - downloadPayment() - uploadPain001Payment() +} +fun main(args: Array<String>) { + PostFinanceCommand().subcommands(Download(), Upload(), GenIncoming()).main(args) } \ No newline at end of file diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt @@ -210,7 +210,11 @@ fun createEbicsRequestForDownloadReceipt( ) XMLUtil.convertJaxbToDocument(req) } - XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) + XMLUtil.signEbicsDocument( + doc, + subscriberDetails.customerAuthPriv, + withEbics3 + ) return XMLUtil.convertDomToString(doc) } @@ -619,6 +623,7 @@ fun parseEbicsHpbOrder(orderDataRaw: ByteArray): HpbResponseData { } private fun ebics3toInternalRepr(response: String): EbicsResponseContent { + // logger.debug("Converting bank resp to internal repr.: $response") val resp: JAXBElement<Ebics3Response> = try { XMLUtil.convertStringToJaxb(response) } catch (e: Exception) { diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Response.kt b/util/src/main/kotlin/ebics_h005/Ebics3Response.kt @@ -108,13 +108,12 @@ class Ebics3Response { @XmlType(name = "DataTransferResponseType", propOrder = ["dataEncryptionInfo", "orderData"]) class DataTransferResponseType { @get:XmlElement(name = "DataEncryptionInfo") - var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null + var dataEncryptionInfo: Ebics3Types.DataEncryptionInfo? = null @get:XmlElement(name = "OrderData", required = true) lateinit var orderData: OrderData } - @XmlAccessorType(XmlAccessType.NONE) @XmlType(name = "ResponseStaticHeaderType", propOrder = ["transactionID", "numSegments"]) class StaticHeaderType { @@ -295,7 +294,6 @@ class Ebics3Response { } } } - fun createForDownloadInitializationPhase( transactionID: String, numSegments: Int, @@ -330,9 +328,9 @@ class Ebics3Response { this.value = "000000" } this.dataTransfer = DataTransferResponseType().apply { - this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { + this.dataEncryptionInfo = Ebics3Types.DataEncryptionInfo().apply { this.authenticate = true - this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest() + this.encryptionPubKeyDigest = Ebics3Types.PubKeyDigest() .apply { this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" this.version = "E002"