diff options
author | Antoine A <> | 2024-04-12 00:38:47 +0200 |
---|---|---|
committer | Antoine A <> | 2024-04-12 00:38:47 +0200 |
commit | a40c7c3bb738a5948aef31778766e3b657cab4fa (patch) | |
tree | 63d19b33383ffd92eac902f56ca33f37e3aae17d | |
parent | 4f413b40ca6e4974e005299ef239d1e96c353501 (diff) | |
download | libeufin-a40c7c3bb738a5948aef31778766e3b657cab4fa.tar.gz libeufin-a40c7c3bb738a5948aef31778766e3b657cab4fa.tar.bz2 libeufin-a40c7c3bb738a5948aef31778766e3b657cab4fa.zip |
Add gls dialect and bounce reserve pub reuse
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 }) |