summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2024-04-12 00:38:47 +0200
committerAntoine A <>2024-04-12 00:38:47 +0200
commita40c7c3bb738a5948aef31778766e3b657cab4fa (patch)
tree63d19b33383ffd92eac902f56ca33f37e3aae17d
parent4f413b40ca6e4974e005299ef239d1e96c353501 (diff)
downloadlibeufin-a40c7c3bb738a5948aef31778766e3b657cab4fa.tar.gz
libeufin-a40c7c3bb738a5948aef31778766e3b657cab4fa.tar.bz2
libeufin-a40c7c3bb738a5948aef31778766e3b657cab4fa.zip
Add gls dialect and bounce reserve pub reuse
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt73
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt29
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt11
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt2
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt51
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt47
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt49
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt60
-rw-r--r--testbench/src/main/kotlin/Main.kt5
9 files changed, 207 insertions, 120 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt
new file mode 100644
index 00000000..59094204
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt
@@ -0,0 +1,73 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 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
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.nexus
+
+import java.nio.file.Path
+import tech.libeufin.common.*
+import tech.libeufin.nexus.ebics.Dialect
+
+val NEXUS_CONFIG_SOURCE = ConfigSource("libeufin", "libeufin-nexus", "libeufin-nexus")
+
+
+class NexusFetchConfig(config: TalerConfig) {
+ val frequency = config.requireDuration("nexus-fetch", "frequency")
+ val ignoreBefore = config.lookupDate("nexus-fetch", "ignore_transactions_before")
+}
+
+/** Configuration for libeufin-nexus */
+class NexusConfig(val config: TalerConfig) {
+ private fun requireString(option: String): String = config.requireString("nexus-ebics", option)
+ private fun requirePath(option: String): Path = config.requirePath("nexus-ebics", option)
+
+ /** The bank's currency */
+ val currency = requireString("currency")
+ /** The bank base URL */
+ val hostBaseUrl = requireString("host_base_url")
+ /** The bank EBICS host ID */
+ val ebicsHostId = requireString("host_id")
+ /** EBICS user ID */
+ val ebicsUserId = requireString("user_id")
+ /** EBICS partner ID */
+ val ebicsPartnerId = requireString("partner_id")
+ /** Bank account metadata */
+ val account = IbanAccountMetadata(
+ iban = requireString("iban"),
+ bic = requireString("bic"),
+ name = requireString("name")
+ )
+ /** Path where we store the bank public keys */
+ val bankPublicKeysPath = requirePath("bank_public_keys_file")
+ /** Path where we store our private keys */
+ val clientPrivateKeysPath = requirePath("client_private_keys_file")
+
+ val fetch = NexusFetchConfig(config)
+ val dialect = when (val type = requireString("bank_dialect")) {
+ "postfinance" -> Dialect.postfinance
+ "gls" -> Dialect.gls
+ else -> throw TalerConfigError.invalid("dialct", "libeufin-nexus", "bank_dialect", "expected 'postfinance' or 'gls' got '$type'")
+ }
+}
+
+fun NexusConfig.checkCurrency(amount: TalerAmount) {
+ if (amount.currency != currency) throw badRequest(
+ "Wrong currency: expected regional $currency got ${amount.currency}",
+ TalerErrorCode.GENERIC_CURRENCY_MISMATCH
+ )
+} \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index 4cd88fbf..fa1e104d 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -119,11 +119,23 @@ suspend fun ingestIncomingPayment(
db: Database,
payment: IncomingPayment
) {
+ suspend fun bounce(msg: String) {
+ val result = db.payment.registerMalformedIncoming(
+ payment,
+ payment.amount,
+ Instant.now()
+ )
+ if (result.new) {
+ logger.info("$payment bounced in '${result.bounceId}': $msg")
+ } else {
+ logger.debug("$payment already seen and bounced in '${result.bounceId}': $msg")
+ }
+ }
runCatching { parseIncomingTxMetadata(payment.wireTransferSubject) }.fold(
onSuccess = { reservePub ->
val res = db.payment.registerTalerableIncoming(payment, reservePub)
when (res) {
- IncomingRegistrationResult.ReservePubReuse -> throw Error("TODO reserve pub reuse")
+ IncomingRegistrationResult.ReservePubReuse -> bounce("reverse pub reuse")
is IncomingRegistrationResult.Success -> {
if (res.new) {
logger.info("$payment")
@@ -133,18 +145,7 @@ suspend fun ingestIncomingPayment(
}
}
},
- onFailure = { e ->
- val result = db.payment.registerMalformedIncoming(
- payment,
- payment.amount,
- Instant.now()
- )
- if (result.new) {
- logger.info("$payment bounced in '${result.bounceId}': ${e.fmt()}")
- } else {
- logger.debug("$payment already seen and bounced in '${result.bounceId}': ${e.fmt()}")
- }
- }
+ onFailure = { e -> bounce(e.fmt())}
)
}
@@ -278,7 +279,7 @@ private suspend fun fetchDocuments(
}
// downloading the content
val doc = doc.doc()
- val order = downloadDocService(doc, doc == SupportedDocument.PAIN_002_LOGS)
+ val order = ctx.cfg.dialect.downloadDoc(doc, false)
ebicsDownload(
ctx.httpClient,
ctx.cfg,
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
index 725f9d5c..1c9ea902 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
@@ -250,14 +250,19 @@ class EbicsSetup: CliktCommand("Set up the EBICS subscriber") {
logger.info("Doing administrative request HKD")
try {
ebicsDownload(client, cfg, clientKeys, bankKeys, EbicsOrder.V3("HKD"), null, null) { stream ->
- val account = EbicsAdministrative.parseHKD(stream)
+ val hkd = EbicsAdministrative.parseHKD(stream)
+ val account = hkd.account
// TODO parse and check more information
if (account.currency != null && account.currency != cfg.currency)
- logger.warn("Expected CURRENCY '${cfg.currency}' from config got '${account.currency}' from bank")
+ logger.error("Expected CURRENCY '${cfg.currency}' from config got '${account.currency}' from bank")
if (account.iban != null && account.iban != cfg.account.iban)
- logger.warn("Expected IBAN '${cfg.account.iban}' from config got '${account.iban}' from bank")
+ logger.error("Expected IBAN '${cfg.account.iban}' from config got '${account.iban}' from bank")
if (account.name != null && account.name != cfg.account.name)
logger.warn("Expected NAME '${cfg.account.name}' from config got '${account.name}' from bank")
+
+ for (order in hkd.orders) {
+ logger.debug("${order.type}${order.params}: ${order.description}")
+ }
}
} catch (e: Exception) {
logger.warn("HKD failed: ${e.fmt()}")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
index 8a54124f..9b1133a4 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
@@ -92,7 +92,7 @@ private suspend fun submitInitiatedPayment(
ctx.cfg,
ctx.clientPrivateKeysFile,
ctx.bankPublicKeysFile,
- uploadPaymentService(),
+ ctx.cfg.dialect.directDebit(),
xml
)
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index 69c96802..b3153a8e 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -52,7 +52,7 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter
import javax.crypto.EncryptedPrivateKeyInfo
-val NEXUS_CONFIG_SOURCE = ConfigSource("libeufin", "libeufin-nexus", "libeufin-nexus")
+
internal val logger: Logger = LoggerFactory.getLogger("libeufin-nexus")
/**
@@ -70,55 +70,6 @@ fun Instant.fmtDate(): String =
fun Instant.fmtDateTime(): String =
DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.of("UTC")).format(this)
-class NexusFetchConfig(config: TalerConfig) {
- val frequency = config.requireDuration("nexus-fetch", "frequency")
- val ignoreBefore = config.lookupDate("nexus-fetch", "ignore_transactions_before")
-}
-
-/** Configuration for libeufin-nexus */
-class NexusConfig(val config: TalerConfig) {
- private fun requireString(option: String): String = config.requireString("nexus-ebics", option)
- private fun requirePath(option: String): Path = config.requirePath("nexus-ebics", option)
-
- /** The bank's currency */
- val currency = requireString("currency")
- /** The bank base URL */
- val hostBaseUrl = requireString("host_base_url")
- /** The bank EBICS host ID */
- val ebicsHostId = requireString("host_id")
- /** EBICS user ID */
- val ebicsUserId = requireString("user_id")
- /** EBICS partner ID */
- val ebicsPartnerId = requireString("partner_id")
- /** Bank account metadata */
- val account = IbanAccountMetadata(
- iban = requireString("iban"),
- bic = requireString("bic"),
- name = requireString("name")
- )
- /** Path where we store the bank public keys */
- val bankPublicKeysPath = requirePath("bank_public_keys_file")
- /** Path where we store our private keys */
- val clientPrivateKeysPath = requirePath("client_private_keys_file")
- /**
- * A name that identifies the EBICS and ISO20022 flavour
- * that Nexus should honor in the communication with the
- * bank.
- */
- val bankDialect: String = requireString("bank_dialect").run {
- if (this != "postfinance") throw Exception("Only 'postfinance' dialect is supported.")
- return@run this
- }
-
- val fetch = NexusFetchConfig(config)
-}
-
-fun NexusConfig.checkCurrency(amount: TalerAmount) {
- if (amount.currency != currency) throw badRequest(
- "Wrong currency: expected regional $currency got ${amount.currency}",
- TalerErrorCode.GENERIC_CURRENCY_MISMATCH
- )
-}
fun Application.nexusApi(db: Database, cfg: NexusConfig) = talerApi(logger) {
wireGatewayApi(db, cfg)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt
index 0d7b26e4..da1e3145 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt
@@ -29,11 +29,20 @@ data class VersionNumber(val number: Float, val schema: String) {
override fun toString(): String = "$number:$schema"
}
-data class AccountInfo(
+data class HKD (
+ val account: AccountInfo,
+ val orders: List<OrderInfo>
+)
+data class AccountInfo (
val currency: String?,
val iban: String?,
val name: String?
)
+data class OrderInfo (
+ val type: String,
+ val params: String,
+ val description: String,
+)
object EbicsAdministrative {
fun HEV(cfg: NexusConfig): ByteArray {
@@ -59,13 +68,12 @@ object EbicsAdministrative {
}
}
- fun parseHKD(stream: InputStream): AccountInfo {
+ fun parseHKD(stream: InputStream): HKD {
return XmlDestructor.fromStream(stream, "HKDResponseOrderData") {
- var currency: String? = null
- var iban: String? = null
- var name: String? = null
one("PartnerInfo") {
- name = opt("AddressInfo")?.one("Name")?.text()
+ var currency: String? = null
+ var iban: String? = null
+ val name = opt("AddressInfo")?.one("Name")?.text()
opt("AccountInfo") {
currency = attr("Currency")
each("AccountNumber") {
@@ -74,8 +82,33 @@ object EbicsAdministrative {
}
}
}
+ val orders = map("OrderInfo") {
+ OrderInfo(
+ one("AdminOrderType").text(),
+ opt("Service") {
+ var params = ""
+ opt("ServiceName")?.run {
+ params += " ${text()}"
+ }
+ opt("Scope")?.run {
+ params += " ${text()}"
+ }
+ opt("ServiceOption")?.run {
+ params += " ${text()}"
+ }
+ opt("MsgName")?.run {
+ params += " ${text()}"
+ }
+ opt("Container")?.run {
+ params += " ${attr("containerType")}"
+ }
+ params
+ } ?: "",
+ one("Description").text()
+ )
+ }
+ HKD(AccountInfo(currency, iban, name), orders)
}
- AccountInfo(currency, iban, name)
}
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt
index 754de501..9210f4cc 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt
@@ -75,19 +75,7 @@ class EbicsBTS(
el("AdminOrderType", order.type)
if (order.type == "BTD") {
el("BTDOrderParams") {
- el("Service") {
- el("ServiceName", order.name!!)
- el("Scope", order.scope!!)
- if (order.container != null) {
- el("Container") {
- attr("containerType", order.container)
- }
- }
- el("MsgName") {
- attr("version", order.messageVersion!!)
- text(order.messageName!!)
- }
- }
+ service(order)
if (startDate != null) {
el("DateRange") {
el("Start", startDate.xmlDate())
@@ -95,6 +83,8 @@ class EbicsBTS(
}
}
}
+ } else {
+ el("StandardOrderParams")
}
}
}
@@ -179,14 +169,7 @@ class EbicsBTS(
is EbicsOrder.V3 -> {
el("AdminOrderType", order.type)
el("BTUOrderParams") {
- el("Service") {
- el("ServiceName", order.name!!)
- el("Scope", order.scope!!)
- el("MsgName") {
- attr("version", order.messageVersion!!)
- text(order.messageName!!)
- }
- }
+ service(order)
el("SignatureFlag", "true")
}
}
@@ -196,9 +179,7 @@ class EbicsBTS(
el("NumSegments", uploadData.segments.size.toString())
}
- el("mutable") {
- el("TransactionPhase", "Initialisation")
- }
+ el("mutable/TransactionPhase", "Initialisation")
}
el("AuthSignature")
el("body") {
@@ -286,6 +267,26 @@ class EbicsBTS(
el("SecurityMedium", "0000")
}
+ private fun XmlBuilder.service(order: EbicsOrder.V3) {
+ el("Service") {
+ el("ServiceName", order.name!!)
+ el("Scope", order.scope!!)
+ if (order.option != null) {
+ el("ServiceOption", order.option)
+ }
+ if (order.container != null) {
+ el("Container") {
+ attr("containerType", order.container)
+ }
+ }
+ el("MsgName") {
+ if (order.messageVersion != null)
+ attr("version", order.messageVersion)
+ text(order.messageName!!)
+ }
+ }
+ }
+
companion object {
fun parseResponse(doc: Document): EbicsResponse<BTSResponse> {
return XmlDestructor.fromDoc(doc, "ebicsResponse") {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt
index 3c73fff0..ba63a115 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt
@@ -32,28 +32,50 @@ sealed class EbicsOrder(val schema: String) {
val messageName: String? = null,
val messageVersion: String? = null,
val container: String? = null,
+ val option: String? = null
): EbicsOrder("H005")
}
-fun downloadDocService(doc: SupportedDocument, ebics2: Boolean): EbicsOrder {
- return if (ebics2) {
- when (doc) {
- SupportedDocument.PAIN_002 -> EbicsOrder.V2_5("Z01", "DZHNN")
- SupportedDocument.CAMT_052 -> EbicsOrder.V2_5("Z52", "DZHNN")
- SupportedDocument.CAMT_053 -> EbicsOrder.V2_5("Z53", "DZHNN")
- SupportedDocument.CAMT_054 -> EbicsOrder.V2_5("Z54", "DZHNN")
- SupportedDocument.PAIN_002_LOGS -> EbicsOrder.V2_5("HAC", "DZHNN")
- }
- } else {
- when (doc) {
- SupportedDocument.PAIN_002 -> EbicsOrder.V3("BTD", "PSR", "CH", "pain.002", "10", "ZIP")
- SupportedDocument.CAMT_052 -> EbicsOrder.V3("BTD", "STM", "CH", "camt.052", "08", "ZIP")
- SupportedDocument.CAMT_053 -> EbicsOrder.V3("BTD", "EOP", "CH", "camt.053", "08", "ZIP")
- SupportedDocument.CAMT_054 -> EbicsOrder.V3("BTD", "REP", "CH", "camt.054", "08", "ZIP")
- SupportedDocument.PAIN_002_LOGS -> EbicsOrder.V3("HAC")
+enum class Dialect {
+ postfinance,
+ gls;
+
+ fun downloadDoc(doc: SupportedDocument, ebics2: Boolean): EbicsOrder {
+ return when (this) {
+ postfinance -> {
+ // TODO test platform need EBICS2 for HAC, should we use a separate dialect ?
+ if (ebics2 || doc == SupportedDocument.PAIN_002_LOGS) {
+ when (doc) {
+ SupportedDocument.PAIN_002 -> EbicsOrder.V2_5("Z01", "DZHNN")
+ SupportedDocument.CAMT_052 -> EbicsOrder.V2_5("Z52", "DZHNN")
+ SupportedDocument.CAMT_053 -> EbicsOrder.V2_5("Z53", "DZHNN")
+ SupportedDocument.CAMT_054 -> EbicsOrder.V2_5("Z54", "DZHNN")
+ SupportedDocument.PAIN_002_LOGS -> EbicsOrder.V2_5("HAC", "DZHNN")
+ }
+ } else {
+ when (doc) {
+ SupportedDocument.PAIN_002 -> EbicsOrder.V3("BTD", "PSR", "CH", "pain.002", "10", "ZIP")
+ SupportedDocument.CAMT_052 -> EbicsOrder.V3("BTD", "STM", "CH", "camt.052", "08", "ZIP")
+ SupportedDocument.CAMT_053 -> EbicsOrder.V3("BTD", "EOP", "CH", "camt.053", "08", "ZIP")
+ SupportedDocument.CAMT_054 -> EbicsOrder.V3("BTD", "REP", "CH", "camt.054", "08", "ZIP")
+ SupportedDocument.PAIN_002_LOGS -> EbicsOrder.V3("HAC")
+ }
+ }
+ }
+ gls -> when (doc) {
+ SupportedDocument.PAIN_002 -> EbicsOrder.V3("BTD", "REP", "DE", "pain.002", null, "ZIP", "SCT")
+ SupportedDocument.CAMT_052 -> EbicsOrder.V3("BTD", "STM", "DE", "camt.052", null, "ZIP")
+ SupportedDocument.CAMT_053 -> EbicsOrder.V3("BTD", "EOP", "DE", "camt.053", null, "ZIP")
+ SupportedDocument.CAMT_054 -> EbicsOrder.V3("BTD", "STM", "DE", "camt.054", null, "ZIP")
+ SupportedDocument.PAIN_002_LOGS -> EbicsOrder.V3("HAC")
+ }
}
}
-}
-fun uploadPaymentService(): EbicsOrder =
- EbicsOrder.V3("BTU", "MCT", "CH", "pain.001", "09")
+ fun directDebit(): EbicsOrder {
+ return when (this) {
+ postfinance -> EbicsOrder.V3("BTU", "MCT", "CH", "pain.001", "09")
+ gls -> EbicsOrder.V3("BTU", "SCT", "DE", "pain.001", null, "XML")
+ }
+ }
+} \ No newline at end of file
diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt
index af61a94f..de475f74 100644
--- a/testbench/src/main/kotlin/Main.kt
+++ b/testbench/src/main/kotlin/Main.kt
@@ -111,6 +111,7 @@ class Cli : CliktCommand("Run integration tests on banks provider") {
val ebicsFlags = "$flags --transient --debug-ebics test/$platform"
val clientKeysPath = cfg.requirePath("nexus-ebics", "client_private_keys_file")
val bankKeysPath = cfg.requirePath("nexus-ebics", "bank_public_keys_file")
+ val currency = cfg.requireString("nexus-ebics", "currency")
// Alternative payto ?
val payto = "payto://iban/CH6208704048981247126?receiver-name=Grothoff%20Hans"
@@ -159,7 +160,7 @@ class Cli : CliktCommand("Run integration tests on banks provider") {
put("txs", suspend {
step("Submit many transaction")
repeat(4) {
- nexusCmd.run("initiate-payment $flags --amount=CHF:${100L+it} --subject \"multi transaction test $it\" \"$payto\"")
+ nexusCmd.run("initiate-payment $flags --amount=$currency:${100L+it} --subject \"multi transaction test $it\" \"$payto\"")
}
nexusCmd.run("ebics-submit $ebicsFlags")
Unit
@@ -168,7 +169,7 @@ class Cli : CliktCommand("Run integration tests on banks provider") {
put("tx", suspend {
step("Submit new transaction")
// TODO interactive payment editor
- nexusCmd.run("initiate-payment $flags \"$payto&amount=CHF:1.1&message=single%20transaction%20test\"")
+ nexusCmd.run("initiate-payment $flags \"$payto&amount=$currency:1.1&message=single%20transaction%20test\"")
nexusCmd.run("ebics-submit $ebicsFlags")
Unit
})