libeufin

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

commit 0c3190f186bcfaee3364e3d178e8bfd38ed995a1
parent 4ff2df763817b66c998da3bd274efd70585d0a85
Author: Antoine A <>
Date:   Tue, 18 Mar 2025 13:36:06 +0100

nexus: clean multi ID logging and clean ebics BTS logic

Diffstat:
Mdatabase-versioning/libeufin-nexus-procedures.sql | 4++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 4++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt | 6+++---
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt | 27++++++++++++++-------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt | 14+++++++-------
Mnexus/src/main/kotlin/tech/libeufin/nexus/test/TxCheck.kt | 35++++++++++++++++-------------------
7 files changed, 45 insertions(+), 47 deletions(-)

diff --git a/database-versioning/libeufin-nexus-procedures.sql b/database-versioning/libeufin-nexus-procedures.sql @@ -107,10 +107,10 @@ out_found=FOUND; IF out_found THEN -- Check metadata -- TODO take subject if missing and more detailed credit payto - IF local_subject IS DISTINCT FROM in_subject THEN + IF in_subject IS NOT NULL AND local_subject != in_subject THEN RAISE NOTICE 'outgoing tx %: stored subject is ''%'' got ''%''', in_end_to_end_id, local_subject, in_subject; END IF; - IF local_credit_payto IS DISTINCT FROM in_credit_payto THEN + IF in_credit_payto IS NOT NULL AND local_credit_payto != in_credit_payto THEN RAISE NOTICE 'outgoing tx %: stored subject credit payto is % got %', in_end_to_end_id, local_credit_payto, in_credit_payto; END IF; IF local_amount IS DISTINCT FROM in_amount THEN diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt @@ -459,7 +459,7 @@ class EbicsFetch: CliktCommand() { val hkd = EbicsAdministrative.parseHKD(stream) val supportedOrder = hkd.partner.orders.map { it.order } logger.debug { - val fmt = supportedOrder.map(EbicsOrder::description).joinToString(", ") + val fmt = supportedOrder.map(EbicsOrder::description).joinToString(" ") "HKD: ${fmt}" } selectedOrder select supportedOrder @@ -478,7 +478,7 @@ class EbicsFetch: CliktCommand() { val orders = client.download(EbicsOrder.V3.HAA, null, null, false) { stream -> val haa = EbicsAdministrative.parseHAA(stream) logger.debug { - val orders = haa.orders.map(EbicsOrder::description).joinToString(", ") + val orders = haa.orders.map(EbicsOrder::description).joinToString(" ") "HAA: ${orders}" } selectedOrder select haa.orders diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt @@ -285,7 +285,7 @@ class EbicsSetup: CliktCommand() { if (account.currency != null && account.currency != cfg.currency) logger.error("Expected CURRENCY '${cfg.currency}' from config got '${account.currency}' from bank") } else if (partner.accounts.isNotEmpty()) { - val ibans = partner.accounts.map { it.iban }.joinToString(", ") + val ibans = partner.accounts.map { it.iban }.joinToString(" ") logger.error("Expected IBAN ${cfg.ebics.account.iban} from config got $ibans from bank") } @@ -294,14 +294,14 @@ class EbicsSetup: CliktCommand() { // Check partner support required orders val unsupportedOrder = requireOrders subtract partner.orders.map { it.order } if (unsupportedOrder.isNotEmpty()) { - logger.warn("Unsupported orders: {}", unsupportedOrder.map(EbicsOrder::description).joinToString(", ")) + logger.warn("Unsupported orders: {}", unsupportedOrder.map(EbicsOrder::description).joinToString(" ")) } // Check user is authorized for required orders if (user != null) { val unauthorizedOrders = requireOrders subtract user.permissions subtract unsupportedOrder if (unauthorizedOrders.isNotEmpty()) { - logger.warn("Unauthorized orders: {}", unauthorizedOrders.map(EbicsOrder::description).joinToString(", ")) + logger.warn("Unauthorized orders: {}", unauthorizedOrders.map(EbicsOrder::description).joinToString(" ")) } logger.info("Subscriber status: {}", user.status.description) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -95,7 +95,7 @@ suspend fun EbicsBTS.postBTS( xmlReq: ByteArray, phase: String, stepLogger: StepLogger? = null -): EbicsResponse<BTSResponse> { +): BTSResponse { val doc = client.postToBank(cfg.host.baseUrl, xmlReq, phase, stepLogger) try { XMLUtil.verifyEbicsDocument( @@ -123,7 +123,7 @@ suspend fun EbicsBTS.postBTS( append(response.bankCode) } } - return response + return response.okOrFail(phase) } /** High level EBICS client */ @@ -173,7 +173,13 @@ class EbicsClient( val tId = db.ebics.first() if (tId == null) break val xml = impl.downloadReceipt(tId, false) - impl.postBTS(client, xml, "Closing pending") + try { + impl.postBTS(client, xml, "Closing pending") + } catch (e: Exception) { + if (e !is EbicsError.Code || e.technicalCode != EbicsReturnCode.EBICS_TX_UNKNOWN_TXID) { + throw e + } + } db.ebics.remove(tId) } @@ -183,8 +189,7 @@ class EbicsClient( val (tId, initContent) = withContext(NonCancellable) { // Init phase val initReq = impl.downloadInitialization(startDate, endDate) - val initResp = impl.postBTS(client, initReq, "Download init", txLog.step("init")) - val initContent = initResp.okOrFail("Download init $description") + val initContent = impl.postBTS(client, initReq, "Download init $description", txLog.step("init")) val tId = requireNotNull(initContent.transactionID) { "Download init $description: missing transaction ID" } @@ -205,8 +210,7 @@ class EbicsClient( val segments = mutableListOf(firstSegment) for (x in 2 .. howManySegments) { val transReq = impl.downloadTransfer(x, howManySegments, tId) - val transResp = impl.postBTS(client, transReq, "Download transfer", txLog.step("transfer$x")) - .okOrFail("Download transfer $description") + val transResp = impl.postBTS(client, transReq, "Download transfer $description", txLog.step("transfer$x")) val segment = requireNotNull(transResp.segment) { "Download transfer: missing encrypted segment" } @@ -238,8 +242,7 @@ class EbicsClient( // First send a proper EBICS transaction receipt val xml = impl.downloadReceipt(tId, res.isSuccess && !peek) - impl.postBTS(client, xml, "Download receipt", txLog.step("receipt")) - .okOrFail("Download receipt $description") + impl.postBTS(client, xml, "Download receipt $description", txLog.step("receipt")) runCatching { db.ebics.remove(tId) } // Then throw business logic exception if any return res.getOrThrow() @@ -264,8 +267,7 @@ class EbicsClient( // Init phase val initXml = impl.uploadInitialization(preparedPayload) - val initResp = impl.postBTS(client, initXml, "Upload init", txLog.step("init")) - .okOrFail("Upload init $description") + val initResp = impl.postBTS(client, initXml, "Upload init $description", txLog.step("init")) val tId = requireNotNull(initResp.transactionID) { "Upload init $description: missing transaction ID" } @@ -278,8 +280,7 @@ class EbicsClient( // Transfer phase for (i in 1..preparedPayload.segments.size) { val transferXml = impl.uploadTransfer(tId, preparedPayload, i) - impl.postBTS(client, transferXml, "Upload transfer", txLog.step("transfer$i")) - .okOrFail("Upload transfer $description") + impl.postBTS(client, transferXml, "Upload transfer $description", txLog.step("transfer$i")) } return orderId } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt @@ -116,7 +116,7 @@ sealed class EbicsOrder(val schema: String) { } infix fun Collection<EbicsOrder>.select(other: Collection<EbicsOrder>): List<EbicsOrder> - = other.filter { order -> this.any { filter -> filter.match(order) } } + = this.flatMap { filter -> other.filter { order -> filter.match(order) } } enum class OrderDoc { /// EBICS acknowledgement - CustomerAcknowledgement HAC pain.002 diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt @@ -51,12 +51,12 @@ data class IncomingId( append(uetr.toString()) } if (txId != null) { - if (length != 1) append(", ") + if (length != 1) append(" ") append("tx=") append(txId) } if (acctSvcrRef != null) { - if (length != 1) append(", ") + if (length != 1) append(" ") append("ref=") append(acctSvcrRef) } @@ -92,12 +92,12 @@ data class OutgoingId( append(msgId.toString()) } if (endToEndId != null) { - if (length != 1) append(", ") + if (length != 1) append(" ") append("e2e=") append(endToEndId) } if (acctSvcrRef != null) { - if (length != 1) append(", ") + if (length != 1) append(" ") append("ref=") append(acctSvcrRef) } @@ -126,7 +126,7 @@ data class BatchId( append(msgId.toString()) } if (acctSvcrRef != null) { - if (length != 1) append(", ") + if (length != 1) append(" ") append("ref=") append(acctSvcrRef) } @@ -445,8 +445,8 @@ private fun XmlDestructor.optBankTransactionCode(): BankTransactionCode? { } /** Parse transaction wire transfer subject */ -private fun XmlDestructor.wireTransferSubject(): String? { - return opt("RmtInf")?.map("Ustrd") { text() }?.joinToString("")?.trim() +private fun XmlDestructor.wireTransferSubject(): String? = opt("RmtInf") { + map("Ustrd") { text() }?.joinToString("")?.trim() } /** Parse account information */ diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/test/TxCheck.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/test/TxCheck.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. + * Copyright (C) 2024-2025 Taler Systems S.A. * * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -24,10 +24,7 @@ import tech.libeufin.common.* import tech.libeufin.nexus.BankPublicKeysFile import tech.libeufin.nexus.ClientPrivateKeysFile import tech.libeufin.nexus.NexusEbicsConfig -import tech.libeufin.nexus.ebics.EbicsBTS -import tech.libeufin.nexus.ebics.EbicsOrder -import tech.libeufin.nexus.ebics.postBTS -import tech.libeufin.nexus.ebics.prepareUploadPayload +import tech.libeufin.nexus.ebics.* import tech.libeufin.nexus.logger data class TxCheckResult( @@ -58,28 +55,28 @@ suspend fun txCheck( suspend fun EbicsBTS.close(id: String, phase: String) { val xml = downloadReceipt(id, false) - postBTS(client, xml, phase).okOrFail(phase) + postBTS(client, xml, phase) } val firstTxId = fetch.postBTS(client, fetch.downloadInitialization(null, null), "Init first fetch") - .okOrFail("Init first fetch") .transactionID!! try { - fetch.postBTS(client, fetch.downloadInitialization(null, null), "Init second fetch").ok()?.run { + try { + val id = fetch.postBTS(client, fetch.downloadInitialization(null, null), "Init second fetch").transactionID!! result.concurrentFetchAndFetch = true - fetch.close(transactionID!!, "Init second fetch") - } + fetch.close(id, "Init second fetch") + } catch (e: EbicsError.Code) {} + var paylod = prepareUploadPayload(cfg, clientKeys, bankKeys, ByteArray(2000000).rand()) - val submitId = submit.postBTS(client, submit.uploadInitialization(paylod), "Init first submit").ok()?.run { - result.concurrentFetchAndSubmit = true - transactionID!! - } - if (submitId != null) { - submit.postBTS(client, submit.uploadTransfer(submitId, paylod, 1), "Submit upload").okOrFail("Submit first upload") - submit.postBTS(client, submit.uploadInitialization(paylod), "Init second submit").ok()?.run { + try { + val submitId = submit.postBTS(client, submit.uploadInitialization(paylod), "Init first submit"). transactionID!! + result.concurrentFetchAndSubmit = true + submit.postBTS(client, submit.uploadTransfer(submitId, paylod, 1), "Submit first upload") + try { + submit.postBTS(client, submit.uploadInitialization(paylod), "Init second submit") result.concurrentSubmitAndSubmit = true - } - } + } catch (e: EbicsError.Code) {} + } catch (e: EbicsError.Code) {} } finally { fetch.close(firstTxId, "Close first fetch") }