libeufin

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

commit c4aac943d0f81327d837584063d8f619d6326257
parent 9b9db0eb58f16fea18f35f221e81a58e90aeca54
Author: Antoine A <>
Date:   Fri, 20 Sep 2024 11:05:45 +0200

Merge remote-tracking branch 'origin/master' into dev/antoine/nexus-dev

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/db/Database.kt | 6+++---
Mcommon/src/main/kotlin/db/notifications.kt | 6+++++-
Mdatabase-versioning/libeufin-bank-procedures.sql | 10+++++-----
Mdatabase-versioning/libeufin-nexus-procedures.sql | 8++++----
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt | 6++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt | 6+++---
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt | 87++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
7 files changed, 86 insertions(+), 43 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -64,19 +64,19 @@ class Database(dbConfig: DatabaseConfig, internal val bankCurrency: String, inte flow.emit(creditRow) } }, - "outgoing_tx" to { + "bank_outgoing_tx" to { val (account, merchant, debitRow, creditRow) = it.split(' ', limit = 4).map { it.toLong() } outgoingTxFlows[account]?.run { flow.emit(debitRow) } }, - "incoming_tx" to { + "bank_incoming_tx" to { val (account, row) = it.split(' ', limit = 2).map { it.toLong() } incomingTxFlows[account]?.run { flow.emit(row) } }, - "withdrawal_status" to { + "bank_withdrawal_status" to { val raw = it.split(' ', limit = 2) val uuid = UUID.fromString(raw[0]) val status = WithdrawalStatus.valueOf(raw[1]) diff --git a/common/src/main/kotlin/db/notifications.kt b/common/src/main/kotlin/db/notifications.kt @@ -60,7 +60,11 @@ fun watchNotifications( conn.getNotifications(0) // Block until we receive at least one notification .forEach { // Dispatch - listeners[it.name]!!(it.parameter) + try { + listeners[it.name]!!(it.parameter) + } catch (e: Exception) { + throw Exception("channel ${it.name} with input '${it.parameter}'", e) + } } } } catch (e: Exception) { diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -567,7 +567,7 @@ INTO local_amount.val, local_amount.frac, local_bank_account_id FROM bank_account_transactions WHERE bank_transaction_id=in_debit_row_id; CALL stats_register_payment('taler_out', NULL, local_amount, null); -- notify new transaction -PERFORM pg_notify('outgoing_tx', in_debtor_account_id || ' ' || in_creditor_account_id || ' ' || in_debit_row_id || ' ' || in_credit_row_id); +PERFORM pg_notify('bank_outgoing_tx', in_debtor_account_id || ' ' || in_creditor_account_id || ' ' || in_debit_row_id || ' ' || in_credit_row_id); END $$; COMMENT ON PROCEDURE register_outgoing IS 'Register a bank transaction as a taler outgoing transaction and announce it'; @@ -605,7 +605,7 @@ IF in_type = 'reserve' THEN CALL stats_register_payment('taler_in', NULL, local_amount, null); END IF; -- Notify new incoming transaction -PERFORM pg_notify('incoming_tx', local_bank_account_id || ' ' || in_tx_row_id); +PERFORM pg_notify('bank_incoming_tx', local_bank_account_id || ' ' || in_tx_row_id); END $$; COMMENT ON PROCEDURE register_incoming IS 'Register a bank transaction as a taler incoming transaction and announce it'; @@ -1026,7 +1026,7 @@ IF not_selected THEN WHERE withdrawal_uuid=in_withdrawal_uuid; -- Notify status change - PERFORM pg_notify('withdrawal_status', in_withdrawal_uuid::text || ' selected'); + PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' selected'); END IF; END $$; COMMENT ON FUNCTION select_taler_withdrawal IS 'Set details of a withdrawal operation'; @@ -1049,7 +1049,7 @@ IF NOT FOUND OR out_already_confirmed THEN END IF; -- Notify status change -PERFORM pg_notify('withdrawal_status', in_withdrawal_uuid::text || ' aborted'); +PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' aborted'); END $$; COMMENT ON FUNCTION abort_taler_withdrawal IS 'Abort a withdrawal operation.'; @@ -1146,7 +1146,7 @@ UPDATE taler_withdrawal_operations CALL register_incoming(tx_row_id, 'reserve'::taler_incoming_type, reserve_pub_local, NULL); -- Notify status change -PERFORM pg_notify('withdrawal_status', in_withdrawal_uuid::text || ' confirmed'); +PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' confirmed'); END $$; COMMENT ON FUNCTION confirm_taler_withdrawal IS 'Set a withdrawal operation as confirmed and wire the funds to the exchange.'; diff --git a/database-versioning/libeufin-nexus-procedures.sql b/database-versioning/libeufin-nexus-procedures.sql @@ -167,7 +167,7 @@ IF NOT out_found THEN ELSE INSERT INTO talerable_outgoing_transactions(outgoing_transaction_id, wtid, exchange_base_url) VALUES (out_tx_id, in_wtid, in_exchange_url); - PERFORM pg_notify('outgoing_tx', out_tx_id::text); + PERFORM pg_notify('nexus_outgoing_tx', out_tx_id::text); END IF; END IF; @@ -295,7 +295,7 @@ ELSE ,in_debit_payto ,in_bank_id ) RETURNING incoming_transaction_id INTO out_tx_id; - PERFORM pg_notify('revenue_tx', out_tx_id::text); + PERFORM pg_notify('nexus_revenue_tx', out_tx_id::text); END IF; -- Register as talerable @@ -312,7 +312,7 @@ IF in_type IS NOT NULL AND NOT EXISTS(SELECT FROM talerable_incoming_transaction ,in_reserve_pub ,in_account_pub ); - PERFORM pg_notify('incoming_tx', out_tx_id::text); + PERFORM pg_notify('nexus_incoming_tx', out_tx_id::text); END IF; END $$; @@ -448,7 +448,7 @@ INSERT INTO transfer_operations( ,in_exchange_base_url ); out_timestamp = in_timestamp; -PERFORM pg_notify('outgoing_tx', out_tx_row_id::text); +PERFORM pg_notify('nexus_outgoing_tx', out_tx_row_id::text); END $$; CREATE FUNCTION batch_outgoing_transactions( diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt @@ -232,9 +232,15 @@ class EbicsSetup: CliktCommand() { if (account.name != null && account.name != cfg.account.name) logger.warn("Expected NAME '${cfg.account.name}' from config got '${account.name}' from bank") + logger.debug("User status: ${hkd.status.name} ${hkd.status.description}") + logger.debug("Supported orders:") for (order in hkd.orders) { logger.debug("${order.type}${order.params}: ${order.description}") } + logger.debug("Authorized orders:") + for (order in hkd.permissions) { + logger.debug("${order.type}${order.params}") + } } } } catch (e: Exception) { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt @@ -79,15 +79,15 @@ class Database(dbConfig: DatabaseConfig, val bankCurrency: String): DbPool(dbCon init { watchNotifications(pgSource, "libeufin_nexus", LoggerFactory.getLogger("libeufin-nexus-db-watcher"), mapOf( - "revenue_tx" to { + "nexus_revenue_tx" to { val id = it.toLong() revenueTxFlows.emit(id) }, - "outgoing_tx" to { + "nexus_outgoing_tx" to { val id = it.toLong() outgoingTxFlows.emit(id) }, - "incoming_tx" to { + "nexus_incoming_tx" to { val id = it.toLong() incomingTxFlows.emit(id) } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt @@ -31,7 +31,9 @@ data class VersionNumber(val number: Float, val schema: String) { data class HKD ( val account: AccountInfo, - val orders: List<OrderInfo> + val orders: List<OrderInfo>, + val permissions: List<OrderInfo>, + val status: UserStatus ) data class AccountInfo ( val currency: String?, @@ -43,6 +45,17 @@ data class OrderInfo ( val params: String, val description: String, ) +// TODO use this in ebics setup to get current state and required actions +enum class UserStatus(val description: String) { + Ready("Subscriber is permitted access"), + New("Subscriber is established, pending access permission"), + INI("Subscriber has sent INI file, but no HIA file yet"), + HIA("Subscriber has sent HIA order, but no INI file yet"), + Initialised("Subscriber has sent both HIA order and INI file"), + SuspendedFailedAttempts("Suspended after several failed attempts, new initialisation via INI and HIA possible"), + SuspendedSPR("Suspended after SPR order, new initialisation via INI and HIA possible"), + SuspendedBank("Suspended by bank, new initialisation via INI and HIA is not possible, suspension can only be revoked by the bank"), +} object EbicsAdministrative { fun HEV(cfg: NexusEbicsConfig): ByteArray { @@ -69,8 +82,33 @@ object EbicsAdministrative { } fun parseHKD(stream: InputStream): HKD { + fun XmlDestructor.orderInfo() = OrderInfo( + one("AdminOrderType").text(), + opt("Service") { + // TODO user a structured type to enable comparison + val params = StringBuilder() + opt("ServiceName")?.run { + params.append(" ${text()}") + } + opt("Scope")?.run { + params.append(" ${text()}") + } + opt("ServiceOption")?.run { + params.append(" ${text()}") + } + opt("MsgName")?.run { + params.append(" ${text()}") + } + opt("Container")?.run { + params.append(" ${attr("containerType")}") + } + params.toString() + } ?: "", + opt("Description")?.text() ?: "" + ) + // TODO handle multiple partner, accounts and user using their respective ids return XmlDestructor.fromStream(stream, "HKDResponseOrderData") { - one("PartnerInfo") { + val (account, orders) = one("PartnerInfo") { var currency: String? = null var iban: String? = null val name = opt("AddressInfo")?.one("Name")?.text() @@ -82,33 +120,28 @@ object EbicsAdministrative { } } } - val orders = map("OrderInfo") { - OrderInfo( - one("AdminOrderType").text(), - opt("Service") { - val params = StringBuilder() - opt("ServiceName")?.run { - params.append(" ${text()}") - } - opt("Scope")?.run { - params.append(" ${text()}") - } - opt("ServiceOption")?.run { - params.append(" ${text()}") - } - opt("MsgName")?.run { - params.append(" ${text()}") - } - opt("Container")?.run { - params.append(" ${attr("containerType")}") - } - params.toString() - } ?: "", - one("Description").text() - ) + val orders = map("OrderInfo") { orderInfo() } + Pair(AccountInfo(currency, iban, name), orders) + } + val (permissions, status) = one("UserInfo") { + val userId = one("UserID").text() + val status = when (val status = one("UserID").attr("Status")) { + "1" -> UserStatus.Ready + "2" -> UserStatus.New + "3" -> UserStatus.INI + "4" -> UserStatus.HIA + "5" -> UserStatus.Initialised + "6" -> UserStatus.SuspendedFailedAttempts + // 7 is not applicable per spec + "8" -> UserStatus.SuspendedSPR + "9" -> UserStatus.SuspendedBank + else -> throw Exception("Unknown user statte $status") } - HKD(AccountInfo(currency, iban, name), orders) + val userName = opt("Name")?.text() + val permissions = map("Permission") { orderInfo() } + Pair(permissions, status) } + HKD(account, orders, permissions, status) } } }