commit a1820a97aaef36e0da8b7873dfacfa5363342045
parent 8a74c8241cb89984e044b4d6136a4dc0763773f3
Author: MS <ms@taler.net>
Date: Wed, 15 Feb 2023 15:04:03 +0100
addressing #6988
Diffstat:
6 files changed, 55 insertions(+), 11 deletions(-)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
@@ -58,14 +58,22 @@ interface BankConnectionProtocol {
// Send to the bank a previously prepared payment instruction.
suspend fun submitPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long)
- // Downloads transactions from the bank, according to the specification
- // given in the arguments.
+ /**
+ * Downloads transactions from the bank, according to the specification
+ * given in the arguments.
+ *
+ * This function returns a possibly empty list of exceptions.
+ * That helps not to stop fetching if ONE operation fails. Notably,
+ * C52 and C53 may be asked along one invocation of this function,
+ * therefore storing the exception on C52 allows the C53 to still
+ * take place. The caller then decides how to handle the exceptions.
+ */
suspend fun fetchTransactions(
fetchSpec: FetchSpecJson,
client: HttpClient,
bankConnectionId: String,
accountId: String
- )
+ ): List<Exception>?
}
fun getConnectionPlugin(connId: String): BankConnectionProtocol {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
@@ -502,7 +502,6 @@ private suspend fun historyIncoming(call: ApplicationCall) {
* In the future, a dedicate "add-incoming" facade should
* be provided, offering the mean to store the credentials
* at configuration time.
- *
*/
private suspend fun addIncoming(call: ApplicationCall) {
val facadeId = ensureNonNull(call.parameters["fcid"])
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -153,7 +153,15 @@ data class CamtTransactionsCount(
* Total number of transactions that were included in a report
* or a statement.
*/
- val downloadedTransactions: Int
+ val downloadedTransactions: Int,
+ /**
+ * Exceptions occurred while fetching transactions. Fetching
+ * transactions can be done via multiple EBICS messages, therefore
+ * a failing one should not prevent other messages to be sent.
+ * This list collects all the exceptions that happened during the
+ * execution of a batch of messages.
+ */
+ var errors: List<Exception>? = null
)
/**
@@ -426,7 +434,7 @@ suspend fun fetchBankAccountTransactions(
* document into the database. This function tries to download
* both reports AND statements even if the first one fails.
*/
- getConnectionPlugin(res.connectionType).fetchTransactions(
+ val errors: List<Exception>? = getConnectionPlugin(res.connectionType).fetchTransactions(
fetchSpec,
client,
res.connectionName,
@@ -437,6 +445,7 @@ suspend fun fetchBankAccountTransactions(
ingestFacadeTransactions(accountId, "taler-wire-gateway", ::talerFilter, ::maybeTalerRefunds)
ingestFacadeTransactions(accountId, "anastasis", ::anastasisFilter, null)
+ ingestionResult.errors = errors
return ingestionResult
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
@@ -239,9 +239,7 @@ suspend fun doEbicsDownloadTransaction(
return EbicsDownloadSuccessResult(respPayload)
}
-/**
- * Currently only 1-segment requests.
- */
+// Currently only 1-segment requests.
suspend fun doEbicsUploadTransaction(
client: HttpClient,
subscriberDetails: EbicsClientSubscriberDetails,
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -402,13 +402,20 @@ fun formatHex(ba: ByteArray): String {
return out
}
+/**
+ * This function returns a possibly empty list of Exception.
+ * That helps not to stop fetching if ONE operation fails. Notably,
+ * C52 and C53 may be asked along one invocation of this function,
+ * therefore storing the exception on C52 allows the C53 to still
+ * take place. The caller then decides how to handle the exceptions.
+ */
class EbicsBankConnectionProtocol: BankConnectionProtocol {
override suspend fun fetchTransactions(
fetchSpec: FetchSpecJson,
client: HttpClient,
bankConnectionId: String,
accountId: String
- ) {
+ ): List<Exception>? {
val subscriberDetails = transaction { getEbicsSubscriberDetails(bankConnectionId) }
val lastTimes = transaction {
val acct = NexusBankAccountEntity.findByName(accountId)
@@ -507,6 +514,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol {
}
}
// Downloads and stores the bank message into the database. No ingestion.
+ val errors = mutableListOf<Exception>()
for (spec in specs) {
try {
fetchEbicsC5x(
@@ -518,8 +526,12 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol {
)
} catch (e: Exception) {
logger.warn("Fetching transactions (${spec.orderType}) excepted: ${e.message}.")
+ errors.add(e)
}
}
+ if (errors.size > 0)
+ return errors
+ return null
}
/**
* Submit one Pain.001 for one payment initiations.
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -729,7 +729,25 @@ val nexusApp: Application.() -> Unit = {
)
}
val ingestionResult = fetchBankAccountTransactions(client, fetchSpec, accountid)
- call.respond(ingestionResult)
+ var statusCode = HttpStatusCode.OK
+ /**
+ * Client errors are unlikely here, because authentication
+ * and JSON validity fail earlier. Hence either Nexus or the
+ * bank had a problem. NOTE: because this handler triggers multiple
+ * fetches, it is ALSO possible that although one error is reported,
+ * SOME transactions made it to the database!
+ */
+ if (ingestionResult.errors != null)
+ /**
+ * 500 is intentionally generic, because multiple errors
+ * may suggest different statuses. The response body however
+ * informs the client about what failed.
+ */
+ statusCode = HttpStatusCode.InternalServerError
+ call.respond(
+ status = statusCode,
+ ingestionResult
+ )
return@post
}