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:
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 {