libeufin

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

commit 4d90ca8d61974ff8cda5c829a9f0a5d25a98bbeb
parent 807eb3fa7eb4a0d555646865a370f5f66cbaa4fc
Author: Antoine A <>
Date:   Wed,  6 Mar 2024 13:12:18 +0100

Supports graceful shutdown using coroutine cancelation

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 203+++++++++++++++++++++++++++++++++++++------------------------------------------
Mcommon/src/main/kotlin/Cli.kt | 14++++++++++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt | 16++++++----------
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt | 38+++++++++++++++++---------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt | 14++++++--------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 38+++++++++++++++++---------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt | 175+++++++++++++++++++++++++++++++++++++++++++------------------------------------
7 files changed, 249 insertions(+), 249 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -45,7 +45,6 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.utils.io.* -import kotlinx.coroutines.runBlocking import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import org.postgresql.util.PSQLState @@ -262,23 +261,21 @@ class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name = val cfg = config.loadDbConfig() val ctx = config.loadBankConfig() Database(cfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db -> - runBlocking { - db.conn { conn -> - if (requestReset) { - resetDatabaseTables(conn, cfg, sqlFilePrefix = "libeufin-bank") - } - initializeDatabaseTables(conn, cfg, sqlFilePrefix = "libeufin-bank") - } - // Create admin account if missing - val res = createAdminAccount(db, ctx) // logs provided by the helper - when (res) { - AccountCreationResult.BonusBalanceInsufficient -> {} - AccountCreationResult.LoginReuse -> {} - AccountCreationResult.PayToReuse -> - throw Exception("Failed to create admin's account") - AccountCreationResult.Success -> - logger.info("Admin's account created") + db.conn { conn -> + if (requestReset) { + resetDatabaseTables(conn, cfg, sqlFilePrefix = "libeufin-bank") } + initializeDatabaseTables(conn, cfg, sqlFilePrefix = "libeufin-bank") + } + // Create admin account if missing + val res = createAdminAccount(db, ctx) // logs provided by the helper + when (res) { + AccountCreationResult.BonusBalanceInsufficient -> {} + AccountCreationResult.LoginReuse -> {} + AccountCreationResult.PayToReuse -> + throw Exception("Failed to create admin's account") + AccountCreationResult.Success -> + logger.info("Admin's account created") } } } @@ -293,30 +290,28 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") val dbCfg = cfg.loadDbConfig() val serverCfg = cfg.loadServerConfig() Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db -> - runBlocking { - if (ctx.allowConversion) { - logger.info("Ensure exchange account exists") - val info = db.account.bankInfo("exchange", ctx.payto) - if (info == null) { - throw Exception("Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled") - } else if (!info.isTalerExchange) { - throw Exception("Account is not an exchange: an exchange account named 'exchange' is required for conversion to be enabled") - } - logger.info("Ensure conversion is enabled") - val sqlProcedures = Path("${dbCfg.sqlDir}/libeufin-conversion-setup.sql") - if (!sqlProcedures.exists()) { - throw Exception("Missing libeufin-conversion-setup.sql file") - } - db.conn { it.execSQLUpdate(sqlProcedures.readText()) } - } else { - logger.info("Ensure conversion is disabled") - val sqlProcedures = Path("${dbCfg.sqlDir}/libeufin-conversion-drop.sql") - if (!sqlProcedures.exists()) { - throw Exception("Missing libeufin-conversion-drop.sql file") - } - db.conn { it.execSQLUpdate(sqlProcedures.readText()) } - // Remove conversion info from the database ? + if (ctx.allowConversion) { + logger.info("Ensure exchange account exists") + val info = db.account.bankInfo("exchange", ctx.payto) + if (info == null) { + throw Exception("Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled") + } else if (!info.isTalerExchange) { + throw Exception("Account is not an exchange: an exchange account named 'exchange' is required for conversion to be enabled") + } + logger.info("Ensure conversion is enabled") + val sqlProcedures = Path("${dbCfg.sqlDir}/libeufin-conversion-setup.sql") + if (!sqlProcedures.exists()) { + throw Exception("Missing libeufin-conversion-setup.sql file") } + db.conn { it.execSQLUpdate(sqlProcedures.readText()) } + } else { + logger.info("Ensure conversion is disabled") + val sqlProcedures = Path("${dbCfg.sqlDir}/libeufin-conversion-drop.sql") + if (!sqlProcedures.exists()) { + throw Exception("Missing libeufin-conversion-drop.sql file") + } + db.conn { it.execSQLUpdate(sqlProcedures.readText()) } + // Remove conversion info from the database ? } val env = applicationEngineEnvironment { @@ -354,16 +349,14 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") { val ctx = cfg.loadBankConfig() val dbCfg = cfg.loadDbConfig() Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db -> - runBlocking { - val res = db.account.reconfigPassword(username, password, null, true) - when (res) { - AccountPatchAuthResult.UnknownAccount -> - throw Exception("Password change for '$username' account failed: unknown account") - AccountPatchAuthResult.OldPasswordMismatch, - AccountPatchAuthResult.TanRequired -> { /* Can never happen */ } - AccountPatchAuthResult.Success -> - logger.info("Password change for '$username' account succeeded") - } + val res = db.account.reconfigPassword(username, password, null, true) + when (res) { + AccountPatchAuthResult.UnknownAccount -> + throw Exception("Password change for '$username' account failed: unknown account") + AccountPatchAuthResult.OldPasswordMismatch, + AccountPatchAuthResult.TanRequired -> { /* Can never happen */ } + AccountPatchAuthResult.Success -> + logger.info("Password change for '$username' account succeeded") } } } @@ -400,33 +393,31 @@ class EditAccount : CliktCommand( val ctx = cfg.loadBankConfig() val dbCfg = cfg.loadDbConfig() Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db -> - runBlocking { - val req = AccountReconfiguration( - name = name, - is_taler_exchange = exchange, - is_public = is_public, - contact_data = ChallengeContactData( - // PATCH semantic, if not given do not change, if empty remove - email = if (email == null) Option.None else Option.Some(if (email != "") email else null), - phone = if (phone == null) Option.None else Option.Some(if (phone != "") phone else null), - ), - cashout_payto_uri = Option.Some(cashout_payto_uri), - debit_threshold = debit_threshold - ) - when (patchAccount(db, ctx, req, username, true, true)) { - AccountPatchResult.Success -> - logger.info("Account '$username' edited") - AccountPatchResult.UnknownAccount -> - throw Exception("Account '$username' not found") - AccountPatchResult.MissingTanInfo -> - throw Exception("missing info for tan channel ${req.tan_channel.get()}") - AccountPatchResult.NonAdminName, - AccountPatchResult.NonAdminCashout, - AccountPatchResult.NonAdminDebtLimit, - is AccountPatchResult.TanRequired -> { - // Unreachable as we edit account as admin - } - } + val req = AccountReconfiguration( + name = name, + is_taler_exchange = exchange, + is_public = is_public, + contact_data = ChallengeContactData( + // PATCH semantic, if not given do not change, if empty remove + email = if (email == null) Option.None else Option.Some(if (email != "") email else null), + phone = if (phone == null) Option.None else Option.Some(if (phone != "") phone else null), + ), + cashout_payto_uri = Option.Some(cashout_payto_uri), + debit_threshold = debit_threshold + ) + when (patchAccount(db, ctx, req, username, true, true)) { + AccountPatchResult.Success -> + logger.info("Account '$username' edited") + AccountPatchResult.UnknownAccount -> + throw Exception("Account '$username' not found") + AccountPatchResult.MissingTanInfo -> + throw Exception("missing info for tan channel ${req.tan_channel.get()}") + AccountPatchResult.NonAdminName, + AccountPatchResult.NonAdminCashout, + AccountPatchResult.NonAdminDebtLimit, + is AccountPatchResult.TanRequired -> { + // Unreachable as we edit account as admin + } } } } @@ -479,37 +470,35 @@ class CreateAccount : CliktCommand( val dbCfg = cfg.loadDbConfig() Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db -> - runBlocking { - val req = json ?: options?.run { - RegisterAccountRequest( - username = username, - password = password, - name = name, - is_public = is_public, - is_taler_exchange = exchange, - contact_data = ChallengeContactData( - email = Option.Some(email), - phone = Option.Some(phone), - ), - cashout_payto_uri = cashout_payto_uri, - payto_uri = payto_uri, - debit_threshold = debit_threshold - ) - } - req?.let { - val (result, internalPayto) = createAccount(db, ctx, req, true) - when (result) { - AccountCreationResult.BonusBalanceInsufficient -> - throw Exception("Insufficient admin funds to grant bonus") - AccountCreationResult.LoginReuse -> - throw Exception("Account username reuse '${req.username}'") - AccountCreationResult.PayToReuse -> - throw Exception("Bank internalPayToUri reuse '$internalPayto'") - AccountCreationResult.Success -> - logger.info("Account '${req.username}' created") - } - println(internalPayto) + val req = json ?: options?.run { + RegisterAccountRequest( + username = username, + password = password, + name = name, + is_public = is_public, + is_taler_exchange = exchange, + contact_data = ChallengeContactData( + email = Option.Some(email), + phone = Option.Some(phone), + ), + cashout_payto_uri = cashout_payto_uri, + payto_uri = payto_uri, + debit_threshold = debit_threshold + ) + } + req?.let { + val (result, internalPayto) = createAccount(db, ctx, req, true) + when (result) { + AccountCreationResult.BonusBalanceInsufficient -> + throw Exception("Insufficient admin funds to grant bonus") + AccountCreationResult.LoginReuse -> + throw Exception("Account username reuse '${req.username}'") + AccountCreationResult.PayToReuse -> + throw Exception("Bank internalPayToUri reuse '$internalPayto'") + AccountCreationResult.Success -> + logger.info("Account '${req.username}' created") } + println(internalPayto) } } } diff --git a/common/src/main/kotlin/Cli.kt b/common/src/main/kotlin/Cli.kt @@ -33,6 +33,7 @@ import com.github.ajalt.clikt.parameters.types.path import org.slf4j.Logger import org.slf4j.LoggerFactory import org.slf4j.event.Level +import kotlinx.coroutines.* private val logger: Logger = LoggerFactory.getLogger("libeufin-config") @@ -48,13 +49,22 @@ fun Throwable.fmtLog(logger: Logger) { logger.debug("{}", this) } -fun cliCmd(logger: Logger, level: Level, lambda: () -> Unit) { +fun cliCmd(logger: Logger, level: Level, lambda: suspend () -> Unit) { // Set root log level val root = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger root.level = ch.qos.logback.classic.Level.convertAnSLF4JLevel(level) // Run cli command catching all errors try { - lambda() + runBlocking { + val job = launch { + lambda() + } + Runtime.getRuntime().addShutdownHook(object : Thread() { + override fun run() = runBlocking{ + job.cancelAndJoin() + } + }) + } } catch (e: Throwable) { e.fmtLog(logger) throw ProgramResult(1) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt @@ -444,10 +444,8 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") { LocalDate.parse(pinnedStartVal).atStartOfDay(ZoneId.of("UTC")).toInstant() } else null ctx.pinnedStart = pinnedStartArg - runBlocking { - if (!fetchDocuments(db, ctx, docs)) { - throw Exception("Failed to fetch documents") - } + if (!fetchDocuments(db, ctx, docs)) { + throw Exception("Failed to fetch documents") } } else { val configValue = cfg.config.requireString("nexus-fetch", "frequency") @@ -460,12 +458,10 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") { } else { cfgFrequency } - runBlocking { - do { - fetchDocuments(db, ctx, docs) - delay(((frequency?.inSeconds ?: 0) * 1000).toLong()) - } while (frequency != null) - } + do { + fetchDocuments(db, ctx, docs) + delay(((frequency?.inSeconds ?: 0) * 1000).toLong()) + } while (frequency != null) } } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt @@ -23,7 +23,6 @@ import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.groups.* import com.github.ajalt.clikt.parameters.options.* import io.ktor.client.* -import kotlinx.coroutines.runBlocking import tech.libeufin.common.* import tech.libeufin.common.crypto.* import tech.libeufin.ebics.* @@ -234,29 +233,26 @@ class EbicsSetup: CliktCommand("Set up the EBICS subscriber") { val clientKeys = loadOrGenerateClientKeys(cfg.clientPrivateKeysFilename) val httpClient = HttpClient() // Privs exist. Upload their pubs - runBlocking { - val keysNotSub = !clientKeys.submitted_ini - if ((!clientKeys.submitted_ini) || forceKeysResubmission) - doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.INI) - // Eject PDF if the keys were submitted for the first time, or the user asked. - if (keysNotSub || generateRegistrationPdf) makePdf(clientKeys, cfg) - if ((!clientKeys.submitted_hia) || forceKeysResubmission) - doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.HIA) - } + val keysNotSub = !clientKeys.submitted_ini + if ((!clientKeys.submitted_ini) || forceKeysResubmission) + doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.INI) + // Eject PDF if the keys were submitted for the first time, or the user asked. + if (keysNotSub || generateRegistrationPdf) makePdf(clientKeys, cfg) + if ((!clientKeys.submitted_hia) || forceKeysResubmission) + doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.HIA) + // Checking if the bank keys exist on disk. var bankKeys = loadBankKeys(cfg.bankPublicKeysFilename) if (bankKeys == null) { - runBlocking { - try { - doKeysRequestAndUpdateState( - cfg, - clientKeys, - httpClient, - KeysOrderType.HPB - ) - } catch (e: Exception) { - throw Exception("Could not download bank keys. Send client keys (and/or related PDF document with --generate-registration-pdf) to the bank", e) - } + try { + doKeysRequestAndUpdateState( + cfg, + clientKeys, + httpClient, + KeysOrderType.HPB + ) + } catch (e: Exception) { + throw Exception("Could not download bank keys. Send client keys (and/or related PDF document with --generate-registration-pdf) to the bank", e) } logger.info("Bank keys stored at ${cfg.bankPublicKeysFilename}") bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)!! diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt @@ -242,14 +242,12 @@ class EbicsSubmit : CliktCommand("Submits any initiated payment found in the dat frequency } } - runBlocking { - do { - // TODO error handling - submitBatch(ctx, db) - // TODO take submitBatch taken time in the delay - delay(((frequency?.inSeconds ?: 0) * 1000).toLong()) - } while (frequency != null) - } + do { + // TODO error handling + submitBatch(ctx, db) + // TODO take submitBatch taken time in the delay + delay(((frequency?.inSeconds ?: 0) * 1000).toLong()) + } while (frequency != null) } } } \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -251,18 +251,16 @@ class InitiatePayment: CliktCommand("Initiate an outgoing payment") { } Database(dbCfg.dbConnStr).use { db -> - runBlocking { - db.initiatedPaymentCreate( - InitiatedPayment( - id = -1, - amount = amount, - wireTransferSubject = subject, - creditPaytoUri = payto.toString(), - initiationTime = Instant.now(), - requestUid = requestUid - ) + db.initiatedPaymentCreate( + InitiatedPayment( + id = -1, + amount = amount, + wireTransferSubject = subject, + creditPaytoUri = payto.toString(), + initiationTime = Instant.now(), + requestUid = requestUid ) - } + ) } } } @@ -299,17 +297,15 @@ class FakeIncoming: CliktCommand("Genere a fake incoming payment") { } Database(dbCfg.dbConnStr).use { db -> - runBlocking { - ingestIncomingPayment(db, - IncomingPayment( - amount = amount, - debitPaytoUri = payto.toString(), - wireTransferSubject = subject, - executionTime = Instant.now(), - bankId = bankId - ) + ingestIncomingPayment(db, + IncomingPayment( + amount = amount, + debitPaytoUri = payto.toString(), + wireTransferSubject = subject, + executionTime = Instant.now(), + bankId = bankId ) - } + ) } } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -55,6 +55,7 @@ import java.security.interfaces.RSAPrivateCrtKey import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* +import kotlinx.coroutines.* /** * Available EBICS versions. @@ -285,89 +286,103 @@ suspend fun ebicsDownload( reqXml: ByteArray, isEbics3: Boolean, processing: (InputStream) -> Unit -) { - 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) { - logger.debug("Download content is empty") - return - } - if (initResp.bankReturnCode != EbicsReturnCode.EBICS_OK) { - throw Exception("Download init phase has bank-technical error: ${initResp.bankReturnCode}") - } - val tId = initResp.transactionID - ?: throw EbicsSideException( - "EBICS download init phase did not return a transaction ID, cannot do the transfer phase.", - sideEc = EbicsSideError.EBICS_UPLOAD_TRANSACTION_ID_MISSING - ) - logger.debug("EBICS download transaction passed the init phase, got ID: $tId") - val howManySegments = initResp.numSegments - if (howManySegments == null) { - throw Exception("Init response lacks the quantity of segments, failing.") - } - val ebicsChunks = mutableListOf<String>() - // Getting the chunk(s) - val firstDataChunk = initResp.orderDataEncChunk - ?: throw EbicsSideException( - "OrderData element not found, despite non empty payload, failing.", - sideEc = EbicsSideError.ORDER_DATA_ELEMENT_NOT_FOUND - ) - val dataEncryptionInfo = initResp.dataEncryptionInfo ?: run { - throw EbicsSideException( - "EncryptionInfo element not found, despite non empty payload, failing.", - sideEc = EbicsSideError.ENCRYPTION_INFO_ELEMENT_NOT_FOUND - ) - } - ebicsChunks.add(firstDataChunk) - // proceed with the transfer phase. - for (x in 2 .. howManySegments) { - // request segment number x. - val transReq = if (isEbics3) - createEbics3DownloadTransferPhase(cfg, clientKeys, x, howManySegments, tId) - else createEbics25DownloadTransferPhase(cfg, clientKeys, x, howManySegments, tId) - - val transResp = postEbics(client, cfg, bankKeys, transReq, isEbics3) - if (!areCodesOk(transResp)) { +) = coroutineScope { + val scope = this + // We need to run the logic in a non-cancelable context because we need to send + // a receipt for each open download transaction, otherwise we'll be stuck in an + // error loop until the pending transaction timeout. + // TODO find a way to cancel the pending transaction ? + withContext(NonCancellable) { + 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) { + logger.debug("Download content is empty") + return@withContext + } else if (initResp.bankReturnCode != EbicsReturnCode.EBICS_OK) { + throw Exception("Download init phase has bank-technical error: ${initResp.bankReturnCode}") + } + val tId = initResp.transactionID + ?: throw EbicsSideException( + "EBICS download init phase did not return a transaction ID, cannot do the transfer phase.", + sideEc = EbicsSideError.EBICS_UPLOAD_TRANSACTION_ID_MISSING + ) + logger.debug("EBICS download transaction passed the init phase, got ID: $tId") + val howManySegments = initResp.numSegments + if (howManySegments == null) { + throw Exception("Init response lacks the quantity of segments, failing.") + } + val ebicsChunks = mutableListOf<String>() + // Getting the chunk(s) + val firstDataChunk = initResp.orderDataEncChunk + ?: throw EbicsSideException( + "OrderData element not found, despite non empty payload, failing.", + sideEc = EbicsSideError.ORDER_DATA_ELEMENT_NOT_FOUND + ) + val dataEncryptionInfo = initResp.dataEncryptionInfo ?: run { throw EbicsSideException( - "EBICS transfer segment #$x failed.", - sideEc = EbicsSideError.TRANSFER_SEGMENT_FAILED + "EncryptionInfo element not found, despite non empty payload, failing.", + sideEc = EbicsSideError.ENCRYPTION_INFO_ELEMENT_NOT_FOUND ) } - val chunk = transResp.orderDataEncChunk - if (chunk == null) { - throw Exception("EBICS transfer phase lacks chunk #$x, failing.") + ebicsChunks.add(firstDataChunk) + // proceed with the transfer phase. + for (x in 2 .. howManySegments) { + if (!scope.isActive) break + // request segment number x. + val transReq = if (isEbics3) + createEbics3DownloadTransferPhase(cfg, clientKeys, x, howManySegments, tId) + else createEbics25DownloadTransferPhase(cfg, clientKeys, x, howManySegments, tId) + + val transResp = postEbics(client, cfg, bankKeys, transReq, isEbics3) + if (!areCodesOk(transResp)) { + throw EbicsSideException( + "EBICS transfer segment #$x failed.", + sideEc = EbicsSideError.TRANSFER_SEGMENT_FAILED + ) + } + val chunk = transResp.orderDataEncChunk + if (chunk == null) { + throw Exception("EBICS transfer phase lacks chunk #$x, failing.") + } + ebicsChunks.add(chunk) + } + suspend fun receipt(success: Boolean) { + val receiptXml = if (isEbics3) + createEbics3DownloadReceiptPhase(cfg, clientKeys, tId, success) + else createEbics25DownloadReceiptPhase(cfg, clientKeys, tId, success) + + // Sending the receipt to the bank. + postEbics( + client, + cfg, + bankKeys, + receiptXml, + isEbics3 + ) + } + if (scope.isActive) { + // all chunks gotten, shaping a meaningful response now. + val payloadBytes = decryptAndDecompressPayload( + clientKeys.encryption_private_key, + dataEncryptionInfo, + ebicsChunks + ) + // Process payload + val res = runCatching { + processing(payloadBytes) + } + receipt(res.isSuccess) + + res.getOrThrow() + } else { + receipt(false) + throw CancellationException() } - ebicsChunks.add(chunk) - } - // all chunks gotten, shaping a meaningful response now. - val payloadBytes = decryptAndDecompressPayload( - clientKeys.encryption_private_key, - dataEncryptionInfo, - ebicsChunks - ) - // Process payload - val res = runCatching { - processing(payloadBytes) } - // payload reconstructed, receipt to the bank. - val success = res.isSuccess - val receiptXml = if (isEbics3) - createEbics3DownloadReceiptPhase(cfg, clientKeys, tId, success) - else createEbics25DownloadReceiptPhase(cfg, clientKeys, tId, success) - - // Sending the receipt to the bank. - postEbics( - client, - cfg, - bankKeys, - receiptXml, - isEbics3 - ) - // Receipt didn't throw, can now return the payload. - return res.getOrThrow() + Unit } /** @@ -509,7 +524,7 @@ suspend fun doEbicsUpload( bankKeys: BankPublicKeysFile, orderService: Ebics3Request.OrderDetails.Service, payload: ByteArray, -): EbicsResponseContent { +): EbicsResponseContent = withContext(NonCancellable) { // TODO use a lambda and pass the order detail there for atomicity ? val preparedPayload = prepareUploadPayload(cfg, clientKeys, bankKeys, payload, isEbics3 = true) val initXml = createEbics3RequestForUploadInitialization( @@ -558,5 +573,5 @@ suspend fun doEbicsUpload( bankErrorCode = initResp.bankReturnCode ) // EBICS- and bank-technical codes were both EBICS_OK, success! - return transferResp + transferResp } \ No newline at end of file