libeufin

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

commit 5837035a2e5679f529196ae85bc041d695a69176
parent f83a22f243a315fbc4b4086155c2401845cf334c
Author: Antoine A <>
Date:   Tue, 13 Feb 2024 08:18:28 +0100

Management of failures in the processing of EBICS download transactions

Diffstat:
Mebics/src/main/kotlin/ebics_h004/EbicsRequest.kt | 2+-
Mebics/src/main/kotlin/ebics_h005/Ebics3Request.kt | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt | 26++++++++++++++------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt | 53-----------------------------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt | 44+++++++++++++++++++-------------------------
Mtestbench/src/main/kotlin/Main.kt | 18++++++++++++------
6 files changed, 47 insertions(+), 98 deletions(-)

diff --git a/ebics/src/main/kotlin/ebics_h004/EbicsRequest.kt b/ebics/src/main/kotlin/ebics_h004/EbicsRequest.kt @@ -309,7 +309,7 @@ class EbicsRequest { body = Body().apply { transferReceipt = TransferReceipt().apply { authenticate = true - receiptCode = if (success) 1 else 0 + receiptCode = if (success) 0 else 1 } } } diff --git a/ebics/src/main/kotlin/ebics_h005/Ebics3Request.kt b/ebics/src/main/kotlin/ebics_h005/Ebics3Request.kt @@ -390,7 +390,7 @@ class Ebics3Request { body = Body().apply { transferReceipt = TransferReceipt().apply { authenticate = true - receiptCode = if (success) 1 else 0 + receiptCode = if (success) 0 else 1 } } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt @@ -78,11 +78,12 @@ data class FetchContext( * length is zero. It returns null, if the bank assigned an * error to the EBICS transaction. */ -private suspend inline fun downloadHelper( +private suspend fun <T> downloadHelper( ctx: FetchContext, lastExecutionTime: Instant? = null, - doc: SupportedDocument -): ByteArray { + doc: SupportedDocument, + processing: (ByteArray) -> T +): T? { val isEbics3 = doc != SupportedDocument.PAIN_002_LOGS val initXml = if (isEbics3) { createEbics3DownloadInitialization( @@ -101,14 +102,14 @@ private suspend inline fun downloadHelper( ebics2Req.orderParams ) } - return doEbicsDownload( + return ebicsDownload( ctx.httpClient, ctx.cfg, ctx.clientKeys, ctx.bankKeys, initXml, isEbics3, - tolerateEmptyResult = true + processing ) } @@ -363,13 +364,14 @@ private suspend fun fetchDocuments( } val doc = doc.doc() // downloading the content - val content = downloadHelper(ctx, lastExecutionTime, doc) - if (!content.isEmpty()) { - ctx.fileLogger.logFetch( - content, - doc == SupportedDocument.PAIN_002_LOGS - ) - ingestDocuments(db, ctx.cfg.currency, content, doc) + downloadHelper(ctx, lastExecutionTime, doc) { content -> + if (!content.isEmpty()) { + ctx.fileLogger.logFetch( + content, + doc == SupportedDocument.PAIN_002_LOGS + ) + ingestDocuments(db, ctx.cfg.currency, content, doc) + } } true } catch (e: Exception) { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt @@ -41,59 +41,6 @@ import javax.xml.datatype.DatatypeFactory private val logger: Logger = LoggerFactory.getLogger("libeufin-nexus-ebics2") /** - * Convenience function to download via EBICS with a - * customer message type. - * - * @param messageType EBICS 2.x message type. Defaults - * to HTD, to get general information about the EBICS - * subscriber. - * @param cfg configuration handle - * @param clientKeys client EBICS keys. - * @param bankKeys bank EBICS keys. - * @param client HTTP client handle. - * @return raw XML response, or null upon errors. - */ -suspend fun doEbicsCustomDownload( - messageType: String = "HTD", - cfg: EbicsSetupConfig, - clientKeys: ClientPrivateKeysFile, - bankKeys: BankPublicKeysFile, - client: HttpClient -): ByteArray { - val xmlReq = createEbics25DownloadInit(cfg, clientKeys, bankKeys, messageType) - return doEbicsDownload(client, cfg, clientKeys, bankKeys, xmlReq, false) -} - -/** - * Request EBICS (2.x) HTD to the bank. This message type - * gets the list of bank accounts that are owned by the EBICS - * client. - * - * @param cfg configuration handle - * @param client client EBICS keys. - * @param bankKeys bank EBICS keys. - * @param client HTTP client handle. - * @return internal representation of the HTD response, or - * null in case of errors. - */ -suspend fun fetchBankAccounts( - cfg: EbicsSetupConfig, - clientKeys: ClientPrivateKeysFile, - bankKeys: BankPublicKeysFile, - client: HttpClient -): HTDResponseOrderData? { - val xmlReq = createEbics25DownloadInit(cfg, clientKeys, bankKeys, "HTD") - val bytesResp = doEbicsDownload(client, cfg, clientKeys, bankKeys, xmlReq, false) - return try { - logger.debug("Fetched accounts: $bytesResp") - XMLUtil.convertBytesToJaxb<HTDResponseOrderData>(bytesResp).value - } catch (e: Exception) { - logger.error("Could not parse the HTD payload, detail: ${e.message}") - return null - } -} - -/** * Creates a EBICS 2.5 download init. message. So far only used * to fetch the PostFinance bank accounts. */ diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -230,14 +230,8 @@ fun generateKeysPdf( * * @param clientKeys client keys, used to sign the request. * @param bankKeys bank keys, used to decrypt and validate the response. - * @param xmlBody raw EBICS request in XML. - * @param withEbics3 true in case the communication is EBICS 3, false otherwise. - * @param tolerateEbicsReturnCode EBICS technical return code that may be accepted - * instead of EBICS_OK. That is the case of EBICS_DOWNLOAD_POSTPROCESS_DONE - * along download receipt phases. - * @param tolerateBankReturnCode Business return code that may be accepted instead of - * EBICS_OK. Typically, EBICS_NO_DOWNLOAD_DATA_AVAILABLE is tolerated - * when asking for new incoming payments. + * @param xmlReq raw EBICS request in XML. + * @param isEbics3 true in case the communication is EBICS 3, false * @return [EbicsResponseContent] or throws [EbicsSideException] */ suspend fun postEbics( @@ -274,8 +268,9 @@ private fun areCodesOk(ebicsResponseContent: EbicsResponseContent) = ebicsResponseContent.bankReturnCode == EbicsReturnCode.EBICS_OK /** - * Collects all the steps of an EBICS download transaction. Namely, - * it conducts: init -> transfer -> receipt phases. + * Perform an EBICS download transaction. + * + * It conducts init -> transfer -> processing -> receipt phases. * * @param client HTTP client for POSTing to the bank. * @param cfg configuration handle. @@ -283,32 +278,27 @@ private fun areCodesOk(ebicsResponseContent: EbicsResponseContent) = * @param bankKeys bank EBICS public keys. * @param reqXml raw EBICS XML request of the init phase. * @param isEbics3 true for EBICS 3, false otherwise. - * @param tolerateEmptyResult true if the EC EBICS_NO_DOWNLOAD_DATA_AVAILABLE - * should be tolerated as the bank-technical error, false otherwise. - * @return the bank response as an uncompressed [ByteArray], or null if one - * error took place. Definition of error: any EBICS- or bank-technical - * EC pairs where at least one is not EBICS_OK, or if tolerateEmptyResult - * is true, the bank-technical EC EBICS_NO_DOWNLOAD_DATA_AVAILABLE is allowed - * other than EBICS_OK. If the request tolerates an empty download content, - * then the empty array is returned. The function may throw [EbicsAdditionalErrors]. + * @param processing processing lambda receiving EBICS files as bytes or empty bytes if nothing to download. + * @return T if the transaction was successful and null if the transaction was empty. If the failure is at the EBICS + * level EbicsSideException is thrown else ités the expection of the processing lambda. */ -suspend fun doEbicsDownload( +suspend fun <T> ebicsDownload( client: HttpClient, cfg: EbicsSetupConfig, clientKeys: ClientPrivateKeysFile, bankKeys: BankPublicKeysFile, reqXml: ByteArray, isEbics3: Boolean, - tolerateEmptyResult: Boolean = false -): ByteArray { + processing: (ByteArray) -> T +): T { val initResp = postEbics(client, cfg, bankKeys, reqXml, isEbics3) logger.debug("Download init phase done. EBICS- and bank-technical codes are: ${initResp.technicalReturnCode}, ${initResp.bankReturnCode}") if (initResp.technicalReturnCode != EbicsReturnCode.EBICS_OK) { throw Exception("Download init phase has EBICS-technical error: ${initResp.technicalReturnCode}") } - if (initResp.bankReturnCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE && tolerateEmptyResult) { + if (initResp.bankReturnCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) { logger.debug("Download content is empty") - return ByteArray(0) + return processing(ByteArray(0)) } if (initResp.bankReturnCode != EbicsReturnCode.EBICS_OK) { throw Exception("Download init phase has bank-technical error: ${initResp.bankReturnCode}") @@ -363,8 +353,12 @@ suspend fun doEbicsDownload( dataEncryptionInfo, ebicsChunks ) + // Process payload + val res = runCatching { + processing(payloadBytes) + } // payload reconstructed, receipt to the bank. - val success = true + val success = res.isSuccess val receiptXml = if (isEbics3) createEbics3DownloadReceiptPhase(cfg, clientKeys, tId, success) else createEbics25DownloadReceiptPhase(cfg, clientKeys, tId, success) @@ -378,7 +372,7 @@ suspend fun doEbicsDownload( isEbics3 ) // Receipt didn't throw, can now return the payload. - return payloadBytes + return res.getOrThrow() } /** diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt @@ -53,6 +53,12 @@ fun ask(question: String): String? { return readlnOrNull() } +fun CliktCommandTestResult.result() { + if (statusCode != 0) { + print("\u001b[;31mERROR:\n$output\u001b[0m") + } +} + fun CliktCommandTestResult.assertOk(msg: String? = null) { println("$output") assertEquals(0, statusCode, msg) @@ -130,27 +136,27 @@ class Cli : CliktCommand("Run integration tests on banks provider") { }) put("recover", suspend { step("Recover old transactions") - nexusCmd.test("ebics-fetch $ebicsFlags --pinned-start 2022-01-01 notification").assertOk() + nexusCmd.test("ebics-fetch $ebicsFlags --pinned-start 2022-01-01 notification").result() }) put("fetch", suspend { step("Fetch all documents") - nexusCmd.test("ebics-fetch $ebicsFlags").assertOk() + nexusCmd.test("ebics-fetch $ebicsFlags").result() }) put("ack", suspend { step("Fetch CustomerAcknowledgement") - nexusCmd.test("ebics-fetch $ebicsFlags acknowledgement").assertOk() + nexusCmd.test("ebics-fetch $ebicsFlags acknowledgement").result() }) put("status", suspend { step("Fetch CustomerPaymentStatusReport") - nexusCmd.test("ebics-fetch $ebicsFlags status").assertOk() + nexusCmd.test("ebics-fetch $ebicsFlags status").result() }) put("notification", suspend { step("Fetch BankToCustomerDebitCreditNotification") - nexusCmd.test("ebics-fetch $ebicsFlags notification").assertOk() + nexusCmd.test("ebics-fetch $ebicsFlags notification").result() }) put("submit", suspend { step("Submit pending transactions") - nexusCmd.test("ebics-submit $ebicsFlags").assertOk() + nexusCmd.test("ebics-submit $ebicsFlags").result() }) if (kind.test) { put("reset-keys", suspend {